@vettly/supabase 0.1.0 → 0.1.2

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.
Files changed (2) hide show
  1. package/README.md +361 -163
  2. package/package.json +9 -19
package/README.md CHANGED
@@ -1,266 +1,464 @@
1
1
  # @vettly/supabase
2
2
 
3
- Vettly content moderation for Supabase Edge Functions. Deno-compatible, fetch-based client.
3
+ Vettly decision infrastructure for Supabase Edge Functions. Deno-compatible client with fetch-based transport for serverless environments.
4
+
5
+ ## Why Edge-Native?
6
+
7
+ Supabase Edge Functions run on Deno at the edge. This package provides:
8
+
9
+ - **Deno-compatible** - Pure fetch-based transport, no Node.js dependencies
10
+ - **Edge-optimized** - Minimal cold start, works in Supabase's 2ms startup
11
+ - **Handler utilities** - One-liner Edge Function moderation
12
+ - **Full audit trail** - Every decision recorded with unique ID
4
13
 
5
14
  ## Installation
6
15
 
16
+ ### npm (for bundling)
17
+
7
18
  ```bash
8
19
  npm install @vettly/supabase
9
20
  ```
10
21
 
11
- Or with Deno in your Edge Function:
22
+ ### Deno (direct import)
12
23
 
13
24
  ```typescript
14
25
  import { moderate } from 'npm:@vettly/supabase'
15
26
  ```
16
27
 
17
- ## Quick Start
28
+ ---
29
+
30
+ ## Quick Start - One-Liner
18
31
 
19
- ### One-liner Edge Function
32
+ The fastest way to add moderation to an Edge Function:
20
33
 
21
34
  ```typescript
35
+ // supabase/functions/comments/index.ts
22
36
  import { createModerationHandler } from '@vettly/supabase'
23
37
 
24
- // Moderate all incoming content
25
38
  Deno.serve(createModerationHandler({
26
- policyId: 'default',
27
- onBlock: (result) => new Response(JSON.stringify({
28
- error: 'Content blocked',
29
- reason: result.categories.filter(c => c.flagged)
30
- }), { status: 403, headers: { 'Content-Type': 'application/json' } })
39
+ policyId: 'community-safe',
40
+ onBlock: (result) => new Response(
41
+ JSON.stringify({
42
+ error: 'Content blocked',
43
+ decisionId: result.decisionId,
44
+ categories: result.categories.filter(c => c.triggered)
45
+ }),
46
+ { status: 403, headers: { 'Content-Type': 'application/json' } }
47
+ )
31
48
  }))
32
49
  ```
33
50
 
34
- ### Manual Control
51
+ ---
52
+
53
+ ## Quick Start - Manual Integration
54
+
55
+ For more control:
35
56
 
36
57
  ```typescript
37
- import { moderate } from '@vettly/supabase'
58
+ // supabase/functions/posts/index.ts
59
+ import { createClient } from '@vettly/supabase'
60
+
61
+ const vettly = createClient({
62
+ apiKey: Deno.env.get('VETTLY_API_KEY')!
63
+ })
38
64
 
39
65
  Deno.serve(async (req) => {
40
66
  const { content, userId } = await req.json()
41
67
 
42
- // Moderate content first
43
- const result = await moderate(content, { policyId: 'strict' })
68
+ // Check content
69
+ const result = await vettly.check(content, {
70
+ policyId: 'community-safe',
71
+ metadata: { userId }
72
+ })
44
73
 
45
74
  if (result.action === 'block') {
46
- return new Response('Content blocked', { status: 403 })
75
+ return new Response(
76
+ JSON.stringify({
77
+ error: 'Content blocked',
78
+ decisionId: result.decisionId
79
+ }),
80
+ { status: 403, headers: { 'Content-Type': 'application/json' } }
81
+ )
47
82
  }
48
83
 
49
- // Content is safe, continue with your logic
50
- // await supabase.from('posts').insert({ content, userId })
84
+ // Content allowed - proceed with your logic
85
+ // Store result.decisionId for audit trail
51
86
 
52
- return new Response(JSON.stringify({ success: true }), {
53
- headers: { 'Content-Type': 'application/json' }
54
- })
87
+ return new Response(
88
+ JSON.stringify({ success: true, decisionId: result.decisionId }),
89
+ { headers: { 'Content-Type': 'application/json' } }
90
+ )
55
91
  })
56
92
  ```
57
93
 
58
- ### Middleware Pattern
59
-
60
- ```typescript
61
- import { withModeration } from '@vettly/supabase'
94
+ ---
62
95
 
63
- const handler = withModeration(
64
- async (req, moderationResult) => {
65
- // Content already passed moderation
66
- const { content, userId } = await req.json()
67
-
68
- // Insert into database...
69
- // await supabase.from('posts').insert({ content, userId })
70
-
71
- return new Response(JSON.stringify({
72
- success: true,
73
- moderation: {
74
- decisionId: moderationResult.decisionId,
75
- safe: moderationResult.safe
76
- }
77
- }), { headers: { 'Content-Type': 'application/json' } })
78
- },
79
- { policyId: 'user-content' }
80
- )
81
-
82
- Deno.serve(handler)
83
- ```
96
+ ## API Reference
84
97
 
85
- ## Configuration
98
+ ### `createClient(config)`
86
99
 
87
- ### Environment Variable
100
+ Create a configured Vettly client.
88
101
 
89
- Set `VETTLY_API_KEY` in your Supabase project secrets:
102
+ ```typescript
103
+ import { createClient } from '@vettly/supabase'
90
104
 
91
- ```bash
92
- supabase secrets set VETTLY_API_KEY=your_api_key
105
+ const client = createClient({
106
+ apiKey: Deno.env.get('VETTLY_API_KEY')!,
107
+ apiUrl: 'https://api.vettly.dev' // optional
108
+ })
93
109
  ```
94
110
 
95
- ### Programmatic Configuration
111
+ #### `client.check(content, options)`
96
112
 
97
- ```typescript
98
- import { createClient } from '@vettly/supabase'
113
+ Check text content against a policy.
99
114
 
100
- const vettly = createClient({
101
- apiKey: Deno.env.get('VETTLY_API_KEY')!,
102
- apiUrl: 'https://api.vettly.dev', // optional
103
- timeout: 30000 // optional, in ms
115
+ ```typescript
116
+ const result = await client.check('User-generated text', {
117
+ policyId: 'community-safe',
118
+ metadata: { userId: 'user_123' }
104
119
  })
105
120
 
106
- const result = await vettly.check('Hello world', { policyId: 'default' })
121
+ console.log(result.action) // 'allow' | 'warn' | 'flag' | 'block'
122
+ console.log(result.decisionId) // UUID for audit trail
123
+ console.log(result.categories) // Array of { category, score, triggered }
107
124
  ```
108
125
 
109
- ## API Reference
110
-
111
- ### `moderate(content, options?)`
126
+ #### `client.checkImage(imageUrl, options)`
112
127
 
113
- Moderate text content using the default client.
128
+ Check an image against a policy.
114
129
 
115
130
  ```typescript
116
- const result = await moderate('User generated content', {
117
- policyId: 'strict', // Policy to use (default: 'default')
118
- contentType: 'text', // 'text' | 'image' | 'video'
119
- language: 'en', // ISO 639-1 language code
120
- metadata: { userId: '123' }
121
- })
131
+ // From URL
132
+ const result = await client.checkImage(
133
+ 'https://cdn.example.com/image.jpg',
134
+ { policyId: 'strict' }
135
+ )
122
136
 
123
- // Result:
124
- // {
125
- // decisionId: 'dec_123',
126
- // safe: true,
127
- // flagged: false,
128
- // action: 'allow',
129
- // categories: [{ category: 'hate_speech', score: 0.01, flagged: false }],
130
- // provider: 'openai',
131
- // latency: 150,
132
- // cost: 0.0001
133
- // }
137
+ // From base64
138
+ const result = await client.checkImage(
139
+ 'data:image/jpeg;base64,/9j/4AAQ...',
140
+ { policyId: 'strict' }
141
+ )
134
142
  ```
135
143
 
136
- ### `moderateImage(imageUrl, options?)`
144
+ ---
137
145
 
138
- Moderate an image by URL.
146
+ ### `moderate(content, options)`
147
+
148
+ Quick moderation without creating a client. Uses `VETTLY_API_KEY` environment variable.
139
149
 
140
150
  ```typescript
141
- const result = await moderateImage('https://example.com/image.jpg', {
142
- policyId: 'images'
143
- })
151
+ import { moderate } from '@vettly/supabase'
152
+
153
+ const result = await moderate('User content', { policyId: 'default' })
154
+
155
+ if (result.action === 'block') {
156
+ // Handle blocked content
157
+ }
144
158
  ```
145
159
 
160
+ ---
161
+
146
162
  ### `createModerationHandler(config)`
147
163
 
148
- Create an Edge Function handler that moderates incoming requests.
164
+ Create an Edge Function handler with built-in moderation.
149
165
 
150
166
  ```typescript
151
167
  import { createModerationHandler } from '@vettly/supabase'
152
168
 
153
169
  Deno.serve(createModerationHandler({
154
- apiKey: Deno.env.get('VETTLY_API_KEY'), // Optional, uses env var by default
155
- policyId: 'default',
170
+ // Required
171
+ policyId: 'community-safe',
156
172
 
157
- // Custom content extraction (default: reads req.json().content)
158
- getContent: async (req) => {
159
- const { message } = await req.json()
160
- return message
161
- },
173
+ // Optional: field path in JSON body (default: 'content')
174
+ field: 'content',
162
175
 
163
- // Handle blocked content
164
- onBlock: (result, req) => {
165
- return new Response(JSON.stringify({ blocked: true }), { status: 403 })
166
- },
167
-
168
- // Handle flagged content (logged but allowed)
169
- onFlag: (result, req) => {
170
- console.log('Flagged content:', result.decisionId)
171
- },
172
-
173
- // Handle warnings
174
- onWarn: (result, req) => {
175
- console.log('Warning:', result.categories)
176
- },
177
-
178
- // Handle allowed content
179
- onAllow: (result, req) => {
180
- return new Response(JSON.stringify({ success: true }))
181
- },
182
-
183
- // Handle errors
184
- onError: (error, req) => {
185
- console.error('Moderation error:', error)
186
- return new Response('Error', { status: 500 })
176
+ // Optional: custom block response
177
+ onBlock: (result) => new Response(
178
+ JSON.stringify({ error: 'Blocked', decisionId: result.decisionId }),
179
+ { status: 403, headers: { 'Content-Type': 'application/json' } }
180
+ ),
181
+
182
+ // Optional: handler for allowed content
183
+ onAllow: async (req, result) => {
184
+ const body = await req.json()
185
+
186
+ // Your business logic here
187
+ // result.decisionId available for audit trail
188
+
189
+ return new Response(JSON.stringify({ success: true }), {
190
+ headers: { 'Content-Type': 'application/json' }
191
+ })
187
192
  }
188
193
  }))
189
194
  ```
190
195
 
196
+ ---
197
+
191
198
  ### `withModeration(handler, config)`
192
199
 
193
- Middleware-style handler that moderates content before passing to your handler.
200
+ Wrap an existing Edge Function with moderation.
194
201
 
195
202
  ```typescript
196
203
  import { withModeration } from '@vettly/supabase'
197
204
 
198
- const handler = withModeration(
199
- async (req, moderationResult) => {
200
- // Your handler receives the moderation result
201
- console.log('Decision ID:', moderationResult.decisionId)
202
- console.log('Safe:', moderationResult.safe)
205
+ async function myHandler(req: Request): Promise<Response> {
206
+ const body = await req.json()
203
207
 
204
- // Process the request...
205
- return new Response(JSON.stringify({ success: true }))
206
- },
207
- { policyId: 'user-content' }
208
- )
208
+ // Your existing logic
209
+ await db.posts.create({ content: body.content })
209
210
 
210
- Deno.serve(handler)
211
+ return new Response(JSON.stringify({ success: true }), {
212
+ headers: { 'Content-Type': 'application/json' }
213
+ })
214
+ }
215
+
216
+ Deno.serve(withModeration(myHandler, {
217
+ policyId: 'community-safe',
218
+ field: 'content'
219
+ }))
211
220
  ```
212
221
 
213
- ### `createClient(config)`
222
+ ---
214
223
 
215
- Create a configured Vettly client for more control.
224
+ ## Response Format
225
+
226
+ All moderation methods return:
216
227
 
217
228
  ```typescript
218
- const client = createClient({
219
- apiKey: 'vettly_...',
220
- apiUrl: 'https://api.vettly.dev',
221
- timeout: 30000
229
+ interface ModerationResult {
230
+ decisionId: string // UUID for audit trail
231
+ safe: boolean // True if content passes
232
+ flagged: boolean // True if flagged for review
233
+ action: 'allow' | 'warn' | 'flag' | 'block'
234
+ categories: Array<{
235
+ category: string // e.g., 'hate_speech', 'harassment'
236
+ score: number // 0.0 to 1.0
237
+ triggered: boolean // True if threshold exceeded
238
+ }>
239
+ latency: number // Response time in ms
240
+ }
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Environment Setup
246
+
247
+ ### Supabase Dashboard
248
+
249
+ 1. Go to your project settings
250
+ 2. Navigate to Edge Functions > Secrets
251
+ 3. Add `VETTLY_API_KEY` with your API key
252
+
253
+ ### Local Development
254
+
255
+ Create `.env.local` in your Supabase project:
256
+
257
+ ```bash
258
+ VETTLY_API_KEY=sk_live_...
259
+ ```
260
+
261
+ Or set in `supabase/functions/.env`:
262
+
263
+ ```bash
264
+ VETTLY_API_KEY=sk_live_...
265
+ ```
266
+
267
+ ---
268
+
269
+ ## Examples
270
+
271
+ ### Comments API
272
+
273
+ ```typescript
274
+ // supabase/functions/comments/index.ts
275
+ import { createClient } from '@vettly/supabase'
276
+ import { createClient as createSupabase } from '@supabase/supabase-js'
277
+
278
+ const vettly = createClient({ apiKey: Deno.env.get('VETTLY_API_KEY')! })
279
+ const supabase = createSupabase(
280
+ Deno.env.get('SUPABASE_URL')!,
281
+ Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
282
+ )
283
+
284
+ Deno.serve(async (req) => {
285
+ if (req.method !== 'POST') {
286
+ return new Response('Method not allowed', { status: 405 })
287
+ }
288
+
289
+ const { content, postId, userId } = await req.json()
290
+
291
+ // Moderate content
292
+ const result = await vettly.check(content, {
293
+ policyId: 'comments',
294
+ metadata: { postId, userId }
295
+ })
296
+
297
+ if (result.action === 'block') {
298
+ return new Response(
299
+ JSON.stringify({
300
+ error: 'Comment blocked',
301
+ decisionId: result.decisionId
302
+ }),
303
+ { status: 403, headers: { 'Content-Type': 'application/json' } }
304
+ )
305
+ }
306
+
307
+ // Save comment with audit trail
308
+ const { data, error } = await supabase
309
+ .from('comments')
310
+ .insert({
311
+ content,
312
+ post_id: postId,
313
+ user_id: userId,
314
+ moderation_decision_id: result.decisionId,
315
+ moderation_action: result.action
316
+ })
317
+ .select()
318
+ .single()
319
+
320
+ if (error) {
321
+ return new Response(
322
+ JSON.stringify({ error: error.message }),
323
+ { status: 500, headers: { 'Content-Type': 'application/json' } }
324
+ )
325
+ }
326
+
327
+ return new Response(
328
+ JSON.stringify(data),
329
+ { headers: { 'Content-Type': 'application/json' } }
330
+ )
331
+ })
332
+ ```
333
+
334
+ ### Image Upload Moderation
335
+
336
+ ```typescript
337
+ // supabase/functions/upload-image/index.ts
338
+ import { createClient } from '@vettly/supabase'
339
+
340
+ const vettly = createClient({ apiKey: Deno.env.get('VETTLY_API_KEY')! })
341
+
342
+ Deno.serve(async (req) => {
343
+ const formData = await req.formData()
344
+ const file = formData.get('file') as File
345
+
346
+ // Convert to base64 for moderation
347
+ const buffer = await file.arrayBuffer()
348
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)))
349
+ const dataUri = `data:${file.type};base64,${base64}`
350
+
351
+ // Check image
352
+ const result = await vettly.checkImage(dataUri, { policyId: 'images' })
353
+
354
+ if (result.action === 'block') {
355
+ return new Response(
356
+ JSON.stringify({
357
+ error: 'Image rejected',
358
+ decisionId: result.decisionId,
359
+ categories: result.categories.filter(c => c.triggered)
360
+ }),
361
+ { status: 403, headers: { 'Content-Type': 'application/json' } }
362
+ )
363
+ }
364
+
365
+ // Proceed with upload...
366
+ // Store result.decisionId with the image record
367
+
368
+ return new Response(
369
+ JSON.stringify({ success: true, decisionId: result.decisionId }),
370
+ { headers: { 'Content-Type': 'application/json' } }
371
+ )
222
372
  })
373
+ ```
374
+
375
+ ### Webhook Handler
376
+
377
+ ```typescript
378
+ // supabase/functions/moderation-webhook/index.ts
379
+ Deno.serve(async (req) => {
380
+ const signature = req.headers.get('x-vettly-signature')
381
+ const payload = await req.text()
382
+
383
+ // Verify webhook signature
384
+ // (implement verification as shown in main SDK docs)
223
385
 
224
- // Text moderation
225
- const textResult = await client.check('Hello world', { policyId: 'default' })
386
+ const event = JSON.parse(payload)
226
387
 
227
- // Image moderation
228
- const imageResult = await client.checkImage('https://...', { policyId: 'images' })
388
+ switch (event.type) {
389
+ case 'decision.blocked':
390
+ // Notify moderators
391
+ await notifySlack(`Content blocked: ${event.data.decisionId}`)
392
+ break
393
+
394
+ case 'decision.flagged':
395
+ // Add to review queue
396
+ await addToReviewQueue(event.data)
397
+ break
398
+ }
399
+
400
+ return new Response('OK')
401
+ })
229
402
  ```
230
403
 
404
+ ---
405
+
231
406
  ## Error Handling
232
407
 
233
408
  ```typescript
234
- import { moderate, VettlyError, VettlyAuthError, VettlyRateLimitError } from '@vettly/supabase'
235
-
236
- try {
237
- const result = await moderate('content')
238
- } catch (error) {
239
- if (error instanceof VettlyAuthError) {
240
- console.error('Invalid API key')
241
- } else if (error instanceof VettlyRateLimitError) {
242
- console.error('Rate limited, retry after:', error.retryAfter)
243
- } else if (error instanceof VettlyError) {
244
- console.error('Vettly error:', error.code, error.message)
409
+ import { createClient } from '@vettly/supabase'
410
+
411
+ const vettly = createClient({ apiKey: Deno.env.get('VETTLY_API_KEY')! })
412
+
413
+ Deno.serve(async (req) => {
414
+ try {
415
+ const { content } = await req.json()
416
+ const result = await vettly.check(content, { policyId: 'default' })
417
+
418
+ return new Response(JSON.stringify(result), {
419
+ headers: { 'Content-Type': 'application/json' }
420
+ })
421
+ } catch (error) {
422
+ // Log error but fail open (allow content through)
423
+ console.error('Moderation error:', error)
424
+
425
+ return new Response(
426
+ JSON.stringify({ warning: 'Moderation unavailable', allowed: true }),
427
+ { headers: { 'Content-Type': 'application/json' } }
428
+ )
245
429
  }
246
- }
430
+ })
247
431
  ```
248
432
 
249
- ## TypeScript Types
433
+ ---
434
+
435
+ ## TypeScript Support
436
+
437
+ Full TypeScript support with Deno:
250
438
 
251
439
  ```typescript
252
- import type {
253
- ModerationResult,
254
- ModerationOptions,
255
- Action,
256
- Category,
257
- CategoryScore,
258
- VettlyConfig,
259
- EdgeHandlerOptions,
260
- ModerationHandlerConfig
261
- } from '@vettly/supabase'
440
+ import { createClient, type ModerationResult } from '@vettly/supabase'
441
+
442
+ const vettly = createClient({ apiKey: Deno.env.get('VETTLY_API_KEY')! })
443
+
444
+ Deno.serve(async (req: Request): Promise<Response> => {
445
+ const { content }: { content: string } = await req.json()
446
+
447
+ const result: ModerationResult = await vettly.check(content, {
448
+ policyId: 'community-safe'
449
+ })
450
+
451
+ return new Response(JSON.stringify(result), {
452
+ headers: { 'Content-Type': 'application/json' }
453
+ })
454
+ })
262
455
  ```
263
456
 
264
- ## License
457
+ ---
458
+
459
+ ## Links
265
460
 
266
- MIT
461
+ - [vettly.dev](https://vettly.dev) - Sign up
462
+ - [docs.vettly.dev](https://docs.vettly.dev) - Documentation
463
+ - [Supabase Edge Functions](https://supabase.com/docs/guides/functions) - Supabase docs
464
+ - [@vettly/sdk](https://www.npmjs.com/package/@vettly/sdk) - Core SDK
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@vettly/supabase",
3
- "version": "0.1.0",
4
- "description": "Vettly content moderation for Supabase Edge Functions",
3
+ "version": "0.1.2",
4
+ "description": "Vettly decision infrastructure for Supabase Edge Functions. Deno-compatible, fetch-based client.",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
7
8
  "types": "./dist/index.d.ts",
8
9
  "exports": {
9
10
  ".": {
@@ -21,25 +22,19 @@
21
22
  "dist",
22
23
  "README.md"
23
24
  ],
24
- "scripts": {
25
- "build": "tsup src/index.ts src/edge.ts --format esm,cjs --dts --clean",
26
- "test": "bun test",
27
- "prepublishOnly": "bun run build"
28
- },
29
25
  "keywords": [
26
+ "vettly",
30
27
  "supabase",
31
28
  "edge-functions",
32
29
  "deno",
33
- "content-moderation",
34
- "moderation",
35
- "vettly",
36
- "trust-safety"
30
+ "decision-infrastructure",
31
+ "policy-governance"
37
32
  ],
38
33
  "author": "Vettly",
39
34
  "license": "MIT",
40
35
  "repository": {
41
36
  "type": "git",
42
- "url": "https://github.com/brian-nextaura/vettly-docs.git",
37
+ "url": "https://github.com/nextauralabs/vettly-docs.git",
43
38
  "directory": "packages/supabase"
44
39
  },
45
40
  "homepage": "https://vettly.dev",
@@ -47,11 +42,6 @@
47
42
  "access": "public"
48
43
  },
49
44
  "bugs": {
50
- "url": "https://github.com/brian-nextaura/vettly-docs/issues"
51
- },
52
- "devDependencies": {
53
- "@types/bun": "latest",
54
- "tsup": "^8.0.0",
55
- "typescript": "^5"
45
+ "url": "https://github.com/nextauralabs/vettly-docs/issues"
56
46
  }
57
47
  }