@vettly/supabase 0.1.1 → 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 +364 -48
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -2,37 +2,60 @@
2
2
 
3
3
  Vettly decision infrastructure for Supabase Edge Functions. Deno-compatible client with fetch-based transport for serverless environments.
4
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
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 import directly in Deno:
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
38
  Deno.serve(createModerationHandler({
25
- policyId: 'default',
26
- onBlock: (result) => new Response(JSON.stringify({
27
- error: 'Content blocked',
28
- categories: result.categories
29
- }), { status: 403 })
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
+ )
30
48
  }))
31
49
  ```
32
50
 
33
- ### Manual Integration
51
+ ---
52
+
53
+ ## Quick Start - Manual Integration
54
+
55
+ For more control:
34
56
 
35
57
  ```typescript
58
+ // supabase/functions/posts/index.ts
36
59
  import { createClient } from '@vettly/supabase'
37
60
 
38
61
  const vettly = createClient({
@@ -40,109 +63,402 @@ const vettly = createClient({
40
63
  })
41
64
 
42
65
  Deno.serve(async (req) => {
43
- const { content } = await req.json()
66
+ const { content, userId } = await req.json()
44
67
 
68
+ // Check content
45
69
  const result = await vettly.check(content, {
46
- policyId: 'community-safe'
70
+ policyId: 'community-safe',
71
+ metadata: { userId }
47
72
  })
48
73
 
49
74
  if (result.action === 'block') {
50
- 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
+ )
51
82
  }
52
83
 
53
- return new Response('OK')
84
+ // Content allowed - proceed with your logic
85
+ // Store result.decisionId for audit trail
86
+
87
+ return new Response(
88
+ JSON.stringify({ success: true, decisionId: result.decisionId }),
89
+ { headers: { 'Content-Type': 'application/json' } }
90
+ )
54
91
  })
55
92
  ```
56
93
 
57
- ## API
94
+ ---
95
+
96
+ ## API Reference
58
97
 
59
98
  ### `createClient(config)`
60
99
 
61
100
  Create a configured Vettly client.
62
101
 
63
102
  ```typescript
103
+ import { createClient } from '@vettly/supabase'
104
+
64
105
  const client = createClient({
65
- apiKey: 'sk_live_...',
66
- apiUrl: 'https://api.vettly.dev' // optional
106
+ apiKey: Deno.env.get('VETTLY_API_KEY')!,
107
+ apiUrl: 'https://api.vettly.dev' // optional
108
+ })
109
+ ```
110
+
111
+ #### `client.check(content, options)`
112
+
113
+ Check text content against a policy.
114
+
115
+ ```typescript
116
+ const result = await client.check('User-generated text', {
117
+ policyId: 'community-safe',
118
+ metadata: { userId: 'user_123' }
67
119
  })
68
120
 
69
- // Check text
70
- const result = await client.check('user content', { 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 }
124
+ ```
71
125
 
72
- // Check image
73
- const result = await client.checkImage('https://...', { policyId: 'strict' })
126
+ #### `client.checkImage(imageUrl, options)`
127
+
128
+ Check an image against a policy.
129
+
130
+ ```typescript
131
+ // From URL
132
+ const result = await client.checkImage(
133
+ 'https://cdn.example.com/image.jpg',
134
+ { policyId: 'strict' }
135
+ )
136
+
137
+ // From base64
138
+ const result = await client.checkImage(
139
+ 'data:image/jpeg;base64,/9j/4AAQ...',
140
+ { policyId: 'strict' }
141
+ )
74
142
  ```
75
143
 
144
+ ---
145
+
76
146
  ### `moderate(content, options)`
77
147
 
78
- Quick moderation without creating a client (uses `VETTLY_API_KEY` env var).
148
+ Quick moderation without creating a client. Uses `VETTLY_API_KEY` environment variable.
79
149
 
80
150
  ```typescript
81
151
  import { moderate } from '@vettly/supabase'
82
152
 
83
- const result = await moderate('user content', { policyId: 'default' })
153
+ const result = await moderate('User content', { policyId: 'default' })
154
+
155
+ if (result.action === 'block') {
156
+ // Handle blocked content
157
+ }
84
158
  ```
85
159
 
160
+ ---
161
+
86
162
  ### `createModerationHandler(config)`
87
163
 
88
- Create an Edge Function handler that automatically moderates incoming requests.
164
+ Create an Edge Function handler with built-in moderation.
89
165
 
90
166
  ```typescript
91
- import { createModerationHandler } from '@vettly/supabase/edge'
167
+ import { createModerationHandler } from '@vettly/supabase'
92
168
 
93
- export default createModerationHandler({
169
+ Deno.serve(createModerationHandler({
170
+ // Required
94
171
  policyId: 'community-safe',
95
- field: 'content', // JSON field to moderate
96
- onBlock: (result) => new Response('Blocked', { status: 403 }),
97
- onAllow: (req, result) => {
98
- // Continue processing
172
+
173
+ // Optional: field path in JSON body (default: 'content')
174
+ field: 'content',
175
+
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
+ })
99
192
  }
100
- })
193
+ }))
101
194
  ```
102
195
 
196
+ ---
197
+
103
198
  ### `withModeration(handler, config)`
104
199
 
105
200
  Wrap an existing Edge Function with moderation.
106
201
 
107
202
  ```typescript
108
- import { withModeration } from '@vettly/supabase/edge'
203
+ import { withModeration } from '@vettly/supabase'
109
204
 
110
- const handler = async (req: Request) => {
111
- // Your logic here
112
- return new Response('OK')
205
+ async function myHandler(req: Request): Promise<Response> {
206
+ const body = await req.json()
207
+
208
+ // Your existing logic
209
+ await db.posts.create({ content: body.content })
210
+
211
+ return new Response(JSON.stringify({ success: true }), {
212
+ headers: { 'Content-Type': 'application/json' }
213
+ })
113
214
  }
114
215
 
115
- export default withModeration(handler, {
116
- policyId: 'default',
117
- field: 'body'
118
- })
216
+ Deno.serve(withModeration(myHandler, {
217
+ policyId: 'community-safe',
218
+ field: 'content'
219
+ }))
119
220
  ```
120
221
 
222
+ ---
223
+
121
224
  ## Response Format
122
225
 
226
+ All moderation methods return:
227
+
123
228
  ```typescript
124
229
  interface ModerationResult {
125
- decisionId: string
126
- safe: boolean
127
- flagged: boolean
230
+ decisionId: string // UUID for audit trail
231
+ safe: boolean // True if content passes
232
+ flagged: boolean // True if flagged for review
128
233
  action: 'allow' | 'warn' | 'flag' | 'block'
129
234
  categories: Array<{
130
- category: string
131
- score: number
132
- triggered: boolean
235
+ category: string // e.g., 'hate_speech', 'harassment'
236
+ score: number // 0.0 to 1.0
237
+ triggered: boolean // True if threshold exceeded
133
238
  }>
134
- latency: number
239
+ latency: number // Response time in ms
135
240
  }
136
241
  ```
137
242
 
138
- ## Environment Variables
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
+ ```
139
266
 
140
- Set in your Supabase project settings:
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
+ )
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)
385
+
386
+ const event = JSON.parse(payload)
387
+
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
+ })
402
+ ```
403
+
404
+ ---
405
+
406
+ ## Error Handling
407
+
408
+ ```typescript
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
+ )
429
+ }
430
+ })
431
+ ```
432
+
433
+ ---
434
+
435
+ ## TypeScript Support
436
+
437
+ Full TypeScript support with Deno:
438
+
439
+ ```typescript
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
+ })
455
+ ```
141
456
 
142
- - `VETTLY_API_KEY` - Your Vettly API key (required)
457
+ ---
143
458
 
144
459
  ## Links
145
460
 
146
461
  - [vettly.dev](https://vettly.dev) - Sign up
147
462
  - [docs.vettly.dev](https://docs.vettly.dev) - Documentation
148
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@vettly/supabase",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Vettly decision infrastructure for Supabase Edge Functions. Deno-compatible, fetch-based client.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",