@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.
- package/README.md +361 -163
- package/package.json +9 -19
package/README.md
CHANGED
|
@@ -1,266 +1,464 @@
|
|
|
1
1
|
# @vettly/supabase
|
|
2
2
|
|
|
3
|
-
Vettly
|
|
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
|
-
|
|
22
|
+
### Deno (direct import)
|
|
12
23
|
|
|
13
24
|
```typescript
|
|
14
25
|
import { moderate } from 'npm:@vettly/supabase'
|
|
15
26
|
```
|
|
16
27
|
|
|
17
|
-
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Quick Start - One-Liner
|
|
18
31
|
|
|
19
|
-
|
|
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: '
|
|
27
|
-
onBlock: (result) => new Response(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Quick Start - Manual Integration
|
|
54
|
+
|
|
55
|
+
For more control:
|
|
35
56
|
|
|
36
57
|
```typescript
|
|
37
|
-
|
|
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
|
-
//
|
|
43
|
-
const result = await
|
|
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(
|
|
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
|
|
50
|
-
//
|
|
84
|
+
// Content allowed - proceed with your logic
|
|
85
|
+
// Store result.decisionId for audit trail
|
|
51
86
|
|
|
52
|
-
return new Response(
|
|
53
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
import { withModeration } from '@vettly/supabase'
|
|
94
|
+
---
|
|
62
95
|
|
|
63
|
-
|
|
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
|
-
|
|
98
|
+
### `createClient(config)`
|
|
86
99
|
|
|
87
|
-
|
|
100
|
+
Create a configured Vettly client.
|
|
88
101
|
|
|
89
|
-
|
|
102
|
+
```typescript
|
|
103
|
+
import { createClient } from '@vettly/supabase'
|
|
90
104
|
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
111
|
+
#### `client.check(content, options)`
|
|
96
112
|
|
|
97
|
-
|
|
98
|
-
import { createClient } from '@vettly/supabase'
|
|
113
|
+
Check text content against a policy.
|
|
99
114
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
### `moderate(content, options?)`
|
|
126
|
+
#### `client.checkImage(imageUrl, options)`
|
|
112
127
|
|
|
113
|
-
|
|
128
|
+
Check an image against a policy.
|
|
114
129
|
|
|
115
130
|
```typescript
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
})
|
|
131
|
+
// From URL
|
|
132
|
+
const result = await client.checkImage(
|
|
133
|
+
'https://cdn.example.com/image.jpg',
|
|
134
|
+
{ policyId: 'strict' }
|
|
135
|
+
)
|
|
122
136
|
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
144
|
+
---
|
|
137
145
|
|
|
138
|
-
|
|
146
|
+
### `moderate(content, options)`
|
|
147
|
+
|
|
148
|
+
Quick moderation without creating a client. Uses `VETTLY_API_KEY` environment variable.
|
|
139
149
|
|
|
140
150
|
```typescript
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
-
|
|
155
|
-
policyId: '
|
|
170
|
+
// Required
|
|
171
|
+
policyId: 'community-safe',
|
|
156
172
|
|
|
157
|
-
//
|
|
158
|
-
|
|
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
|
-
//
|
|
164
|
-
onBlock: (result
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
200
|
+
Wrap an existing Edge Function with moderation.
|
|
194
201
|
|
|
195
202
|
```typescript
|
|
196
203
|
import { withModeration } from '@vettly/supabase'
|
|
197
204
|
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
},
|
|
207
|
-
{ policyId: 'user-content' }
|
|
208
|
-
)
|
|
208
|
+
// Your existing logic
|
|
209
|
+
await db.posts.create({ content: body.content })
|
|
209
210
|
|
|
210
|
-
|
|
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
|
-
|
|
222
|
+
---
|
|
214
223
|
|
|
215
|
-
|
|
224
|
+
## Response Format
|
|
225
|
+
|
|
226
|
+
All moderation methods return:
|
|
216
227
|
|
|
217
228
|
```typescript
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
const textResult = await client.check('Hello world', { policyId: 'default' })
|
|
386
|
+
const event = JSON.parse(payload)
|
|
226
387
|
|
|
227
|
-
|
|
228
|
-
|
|
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 {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## TypeScript Support
|
|
436
|
+
|
|
437
|
+
Full TypeScript support with Deno:
|
|
250
438
|
|
|
251
439
|
```typescript
|
|
252
|
-
import type
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
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
|
-
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Links
|
|
265
460
|
|
|
266
|
-
|
|
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.
|
|
4
|
-
"description": "Vettly
|
|
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.
|
|
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
|
-
"
|
|
34
|
-
"
|
|
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/
|
|
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/
|
|
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
|
}
|