mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
examples: migrate Edge Functions to @supabase/server (#46890)
- extends/supersedes: #46665 - towards COM-269 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Standardized Edge Function entrypoints across examples to a consistent `export default` shape, with runtime-provided admin access for storage/database operations. * Updated public endpoint handling to use appropriate auth modes. * **Bug Fixes** * Improved error handling to return structured JSON responses with correct HTTP status codes for invalid requests and failures. * Harmonized local invocation examples to use the right header format. * **Chores** * Updated example `verify_jwt` settings to disable JWT verification for public/demo endpoints. * **Documentation** * Fixed README typo and refreshed invocation curl examples. * **Tests** * None. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Tomas Pozo <tomaspozogarzon@gmail.com>
This commit is contained in:
1 parent
5fc0c86007
commit
d5fde192d5
54 files changed
+1481
-1565
No files matched your search
@@ -13,7 +13,7 @@ file_size_limit = "50MiB"
|
||||
|
||||
[functions.image_gen]
|
||||
enabled = true
|
||||
verify_jwt = true
|
||||
verify_jwt = false
|
||||
# import_map = "./functions/image_gen/deno.json"
|
||||
# Uncomment to specify a custom file path to the entrypoint.
|
||||
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||
|
||||
@@ -1,82 +1,77 @@
|
||||
// AWS SDK issue: https://github.com/aws/aws-sdk-js-v3/issues/6134
|
||||
// We need to mock the file system for the AWS SDK to work.
|
||||
import { prepareVirtualFile } from 'https://deno.land/x/mock_file@v1.1.2/mod.ts'
|
||||
|
||||
import { BedrockRuntimeClient, InvokeModelCommand } from 'npm:@aws-sdk/client-bedrock-runtime'
|
||||
import { createClient } from 'npm:@supabase/supabase-js'
|
||||
import { decode } from 'npm:base64-arraybuffer'
|
||||
import { BedrockRuntimeClient, InvokeModelCommand } from 'npm:@aws-sdk/client-bedrock-runtime@^3'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import { decode } from 'npm:base64-arraybuffer@^1'
|
||||
|
||||
console.log('Hello from Amazon Bedrock!')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
prepareVirtualFile('./aws/config')
|
||||
prepareVirtualFile('./aws/credentials')
|
||||
// Called with a publishable key on the `apikey` header. Deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => {
|
||||
prepareVirtualFile('./aws/config')
|
||||
prepareVirtualFile('./aws/credentials')
|
||||
|
||||
const client = new BedrockRuntimeClient({
|
||||
region: 'us-west-2',
|
||||
credentials: {
|
||||
accessKeyId: Deno.env.get('AWS_ACCESS_KEY_ID') ?? '',
|
||||
secretAccessKey: Deno.env.get('AWS_SECRET_ACCESS_KEY') ?? '',
|
||||
sessionToken: Deno.env.get('AWS_SESSION_TOKEN') ?? '',
|
||||
},
|
||||
})
|
||||
|
||||
const { prompt, seed } = await req.json()
|
||||
console.log(prompt)
|
||||
const input = {
|
||||
contentType: 'application/json',
|
||||
accept: '*/*',
|
||||
modelId: 'amazon.titan-image-generator-v1',
|
||||
body: JSON.stringify({
|
||||
taskType: 'TEXT_IMAGE',
|
||||
textToImageParams: { text: prompt },
|
||||
imageGenerationConfig: {
|
||||
numberOfImages: 1,
|
||||
quality: 'standard',
|
||||
cfgScale: 8.0,
|
||||
height: 512,
|
||||
width: 512,
|
||||
seed: seed ?? 0,
|
||||
const client = new BedrockRuntimeClient({
|
||||
region: 'us-west-2',
|
||||
credentials: {
|
||||
accessKeyId: Deno.env.get('AWS_ACCESS_KEY_ID') ?? '',
|
||||
secretAccessKey: Deno.env.get('AWS_SECRET_ACCESS_KEY') ?? '',
|
||||
sessionToken: Deno.env.get('AWS_SESSION_TOKEN') ?? '',
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const command = new InvokeModelCommand(input)
|
||||
const response = await client.send(command)
|
||||
console.log(response)
|
||||
|
||||
if (response.$metadata.httpStatusCode === 200) {
|
||||
const { body, $metadata } = response
|
||||
|
||||
const textDecoder = new TextDecoder('utf-8')
|
||||
const jsonString = textDecoder.decode(body.buffer)
|
||||
const parsedData = JSON.parse(jsonString)
|
||||
console.log(parsedData)
|
||||
const image = parsedData.images[0]
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
const supabaseClient = createClient(
|
||||
// Supabase API URL - env var exported by default.
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
// Supabase API SECRET KEY - env var exported by default.
|
||||
SUPABASE_SECRET_KEYS['default']!
|
||||
)
|
||||
|
||||
const { data: upload, error: uploadError } = await supabaseClient.storage
|
||||
.from('images')
|
||||
.upload(`${$metadata.requestId ?? ''}.png`, decode(image), {
|
||||
contentType: 'image/png',
|
||||
cacheControl: '3600',
|
||||
upsert: false,
|
||||
})
|
||||
if (!upload) {
|
||||
return Response.json(uploadError)
|
||||
const { prompt, seed } = await req.json()
|
||||
console.log(prompt)
|
||||
const input = {
|
||||
contentType: 'application/json',
|
||||
accept: '*/*',
|
||||
modelId: 'amazon.titan-image-generator-v1',
|
||||
body: JSON.stringify({
|
||||
taskType: 'TEXT_IMAGE',
|
||||
textToImageParams: { text: prompt },
|
||||
imageGenerationConfig: {
|
||||
numberOfImages: 1,
|
||||
quality: 'standard',
|
||||
cfgScale: 8.0,
|
||||
height: 512,
|
||||
width: 512,
|
||||
seed: seed ?? 0,
|
||||
},
|
||||
}),
|
||||
}
|
||||
const { data } = supabaseClient.storage.from('images').getPublicUrl(upload.path!)
|
||||
return Response.json(data)
|
||||
}
|
||||
|
||||
return Response.json(response)
|
||||
})
|
||||
const command = new InvokeModelCommand(input)
|
||||
const response = await client.send(command)
|
||||
console.log(response)
|
||||
|
||||
if (response.$metadata.httpStatusCode === 200) {
|
||||
const { body, $metadata } = response
|
||||
|
||||
const textDecoder = new TextDecoder('utf-8')
|
||||
const jsonString = textDecoder.decode(body.buffer)
|
||||
const parsedData = JSON.parse(jsonString)
|
||||
console.log(parsedData)
|
||||
const image = parsedData.images[0]
|
||||
|
||||
const { data: upload, error: uploadError } = await ctx.supabaseAdmin.storage
|
||||
.from('images')
|
||||
.upload(`${$metadata.requestId ?? ''}.png`, decode(image), {
|
||||
contentType: 'image/png',
|
||||
cacheControl: '3600',
|
||||
upsert: false,
|
||||
})
|
||||
if (!upload) {
|
||||
return Response.json(uploadError)
|
||||
}
|
||||
const { data } = ctx.supabaseAdmin.storage.from('images').getPublicUrl(upload.path!)
|
||||
return Response.json(data)
|
||||
}
|
||||
|
||||
return Response.json(response)
|
||||
}),
|
||||
}
|
||||
|
||||
/* To invoke locally:
|
||||
|
||||
@@ -85,7 +80,7 @@ Deno.serve(async (req) => {
|
||||
3. Make an HTTP request:
|
||||
|
||||
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/image_gen' \
|
||||
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
|
||||
--header 'apikey: <SUPABASE_PUBLISHABLE_KEY>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"prompt":"A beautiful picture of a bird"}'
|
||||
*/
|
||||
@@ -7,7 +7,7 @@ Since Supabase Edge Runtime [v1.36.0](https://github.com/supabase/edge-runtime/r
|
||||
This demo consists of three parts:
|
||||
|
||||
1. A [`generate-embedding`](./supabase/functions/generate-embedding/index.ts) database webhook edge function which generates embeddings when a content row is added (or updated) in the [`public.embeddings`](./supabase/migrations/20240408072601_embeddings.sql) table.
|
||||
2. A [`query_embeddings` Postgres function](./supabase/migrations/20240410031515_vector-search.sql) which allows us to perform similarity search from an egde function via [Remote Procedure Call (RPC)](https://supabase.com/docs/guides/database/functions?language=js).
|
||||
2. A [`query_embeddings` Postgres function](./supabase/migrations/20240410031515_vector-search.sql) which allows us to perform similarity search from an edge function via [Remote Procedure Call (RPC)](https://supabase.com/docs/guides/database/functions?language=js).
|
||||
3. A [`search` edge function](./supabase/functions/search/index.ts) which generates the embedding for the search term, performs the similarity search via RPC function call, and returns the result.
|
||||
|
||||
## Deploy
|
||||
@@ -24,7 +24,7 @@ Run a search via curl POST request:
|
||||
|
||||
```bash
|
||||
curl -i --location --request POST 'https://<PROJECT-REF>.supabase.co/functions/v1/search' \
|
||||
--header 'apikey: <SUPABASE_PUBLISHABLE_KEY>' \
|
||||
--header 'apikey: <SUPABASE_SECRET_KEY>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"search":"vehicles"}'
|
||||
```
|
||||
@@ -33,7 +33,7 @@ sql_paths = ['./seed.sql']
|
||||
|
||||
[functions.generate-embedding]
|
||||
enabled = true
|
||||
# verify_jwt = true
|
||||
verify_jwt = false
|
||||
# import_map = "./functions/generate-embedding/deno.json"
|
||||
# Uncomment to specify a custom file path to the entrypoint.
|
||||
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||
@@ -41,7 +41,7 @@ enabled = true
|
||||
|
||||
[functions.search]
|
||||
enabled = true
|
||||
# verify_jwt = true
|
||||
verify_jwt = false
|
||||
# import_map = "./functions/search/deno.json"
|
||||
# Uncomment to specify a custom file path to the entrypoint.
|
||||
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
|
||||
import { createClient } from 'jsr:@supabase/supabase-js@2'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
import { Database, Tables } from '../_shared/database.types.ts'
|
||||
|
||||
type EmbeddingsRecord = Tables<'embeddings'>
|
||||
@@ -11,38 +12,38 @@ interface WebhookPayload {
|
||||
schema: 'public'
|
||||
old_record: null | EmbeddingsRecord
|
||||
}
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
|
||||
const supabase = createClient<Database>(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
SUPABASE_SECRET_KEYS['default']!
|
||||
)
|
||||
|
||||
const model = new Supabase.ai.Session('gte-small')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
const { content, id } = payload.record
|
||||
// Called with a secret key on the `apikey` header. Deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase<Database>({ auth: 'secret' }, async (req, ctx) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
const { content, id } = payload.record
|
||||
|
||||
// Check if content has changed.
|
||||
if (content === payload?.old_record?.content) {
|
||||
return new Response('ok - no change')
|
||||
}
|
||||
// Check if content has changed.
|
||||
if (content === payload?.old_record?.content) {
|
||||
return Response.json({ status: 'ok - no change' })
|
||||
}
|
||||
|
||||
// Generate embedding
|
||||
const embedding = await model.run(content, {
|
||||
mean_pool: true,
|
||||
normalize: true,
|
||||
})
|
||||
|
||||
// Store in DB
|
||||
const { error } = await supabase
|
||||
.from('embeddings')
|
||||
.update({
|
||||
embedding: JSON.stringify(embedding),
|
||||
// Generate embedding
|
||||
const embedding = await model.run(content, {
|
||||
mean_pool: true,
|
||||
normalize: true,
|
||||
})
|
||||
.eq('id', id)
|
||||
if (error) console.warn(error.message)
|
||||
|
||||
return new Response('ok - updated')
|
||||
})
|
||||
// Store in DB
|
||||
const { error } = await ctx.supabaseAdmin
|
||||
.from('embeddings')
|
||||
.update({
|
||||
embedding: JSON.stringify(embedding),
|
||||
})
|
||||
.eq('id', id)
|
||||
if (error) {
|
||||
console.warn(error.message)
|
||||
return Response.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
|
||||
return Response.json({ status: 'ok - updated' })
|
||||
}),
|
||||
}
|
||||
@@ -1,40 +1,38 @@
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
|
||||
import { createClient } from 'jsr:@supabase/supabase-js@2'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import { Database } from '../_shared/database.types.ts'
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
|
||||
const supabase = createClient<Database>(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
SUPABASE_SECRET_KEYS['default']!
|
||||
)
|
||||
|
||||
const model = new Supabase.ai.Session('gte-small')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const { search } = await req.json()
|
||||
if (!search) return new Response('Please provide a search param!')
|
||||
// Generate embedding for search term.
|
||||
const embedding = await model.run(search, {
|
||||
mean_pool: true,
|
||||
normalize: true,
|
||||
})
|
||||
|
||||
// Query embeddings.
|
||||
const { data: result, error } = await supabase
|
||||
.rpc('query_embeddings', {
|
||||
embedding: JSON.stringify(embedding),
|
||||
match_threshold: 0.8,
|
||||
// Called with a secret key on the `apikey` header. Deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase<Database>({ auth: 'secret' }, async (req, ctx) => {
|
||||
const { search } = await req.json()
|
||||
if (!search) {
|
||||
return Response.json({ error: 'Please provide a search param!' }, { status: 400 })
|
||||
}
|
||||
// Generate embedding for search term.
|
||||
const embedding = await model.run(search, {
|
||||
mean_pool: true,
|
||||
normalize: true,
|
||||
})
|
||||
.select('content')
|
||||
.limit(3)
|
||||
if (error) {
|
||||
return Response.json(error)
|
||||
}
|
||||
|
||||
return Response.json({ search, result })
|
||||
})
|
||||
// Query embeddings.
|
||||
const { data: result, error } = await ctx.supabaseAdmin
|
||||
.rpc('query_embeddings', {
|
||||
embedding: JSON.stringify(embedding),
|
||||
match_threshold: 0.8,
|
||||
})
|
||||
.select('content')
|
||||
.limit(3)
|
||||
if (error) {
|
||||
return Response.json(error)
|
||||
}
|
||||
|
||||
return Response.json({ search, result })
|
||||
}),
|
||||
}
|
||||
|
||||
/* To invoke locally:
|
||||
|
||||
@@ -43,7 +41,7 @@ Deno.serve(async (req) => {
|
||||
3. Make an HTTP request:
|
||||
|
||||
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/search' \
|
||||
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
|
||||
--header 'apikey: <SUPABASE_SECRET_KEY>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"search":"vehicles"}'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- Insert your <PROJECT-REF> and <SUPABASE_PUBLISHABLE_KEY> below, then uncomment the trigger creation and run `supabase db push`
|
||||
-- Insert your <PROJECT-REF> and <SUPABASE_SECRET_KEY> below, then uncomment the trigger creation and run `supabase db push`
|
||||
-- Alternatively, head to the Database Webhook settings https://supabase.com/dashboard/project/_/database/hooks
|
||||
-- Select "Create a new Hook" > Table "public.embeddings" > check "INSERT" & "Update" > Supabase Edge Functions > Add auth header with service key
|
||||
-- Select "Create a new Hook" > Table "public.embeddings" > check "INSERT" & "Update" > Supabase Edge Functions > Add apiKey header with secret key
|
||||
|
||||
-- CREATE TRIGGER "on_inserted_or_updated_embedding"
|
||||
-- AFTER INSERT
|
||||
@@ -9,7 +9,7 @@
|
||||
-- EXECUTE FUNCTION supabase_functions.http_request (
|
||||
-- 'https://<PROJECT-REF>.supabase.co/functions/v1/generate-embedding',
|
||||
-- 'POST',
|
||||
-- '{"Content-type":"application/json","apikey":"<SUPABASE_PUBLISHABLE_KEY>"}',
|
||||
-- '{"Content-type":"application/json","apikey":"<SUPABASE_SECRET_KEY>"}',
|
||||
-- '{}',
|
||||
-- '5000'
|
||||
-- );
|
||||
@@ -24,7 +24,7 @@
|
||||
-- EXECUTE FUNCTION supabase_functions.http_request (
|
||||
-- 'http://kong:8000/functions/v1/generate-embedding',
|
||||
-- 'POST',
|
||||
-- '{"Content-type":"application/json","apikey":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"}',
|
||||
-- '{"Content-type":"application/json","apikey":"<SUPABASE_SECRET_KEY>"}',
|
||||
-- '{}',
|
||||
-- '5000'
|
||||
-- );
|
||||
@@ -24,7 +24,7 @@ enabled = false
|
||||
|
||||
[functions.llamafile]
|
||||
enabled = true
|
||||
# verify_jwt = true
|
||||
verify_jwt = false
|
||||
# import_map = "./functions/llamafile/deno.json"
|
||||
# Uncomment to specify a custom file path to the entrypoint.
|
||||
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||
@@ -32,7 +32,7 @@ enabled = true
|
||||
|
||||
[functions.llamafile-stream]
|
||||
enabled = true
|
||||
# verify_jwt = true
|
||||
verify_jwt = false
|
||||
# import_map = "./functions/llamafile-stream/deno.json"
|
||||
# Uncomment to specify a custom file path to the entrypoint.
|
||||
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||
@@ -40,7 +40,7 @@ enabled = true
|
||||
|
||||
[functions.openai-sdk]
|
||||
enabled = true
|
||||
# verify_jwt = true
|
||||
verify_jwt = false
|
||||
# import_map = "./functions/openai-sdk/deno.json"
|
||||
# Uncomment to specify a custom file path to the entrypoint.
|
||||
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||
|
||||
@@ -1,59 +1,65 @@
|
||||
// https://github.com/Mozilla-Ocho/llamafile?tab=readme-ov-file#quickstart
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
|
||||
const session = new Supabase.ai.Session('LLaMA_CPP')
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
const params = new URL(req.url).searchParams
|
||||
const prompt = params.get('prompt') ?? ''
|
||||
// Called with a publishable key on the `apikey` header. Deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'publishable' }, async (req) => {
|
||||
const params = new URL(req.url).searchParams
|
||||
const prompt = params.get('prompt') ?? ''
|
||||
|
||||
// Get the output as a stream
|
||||
const output = (await session.run(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
mode: 'openaicompatible', // Mode for the inference API host. (default: 'ollama')
|
||||
stream: true,
|
||||
}
|
||||
)) as AsyncGenerator<any>
|
||||
|
||||
const body = new ReadableStream({
|
||||
async pull(ctrl) {
|
||||
try {
|
||||
const item = await output.next()
|
||||
|
||||
if (item.done) {
|
||||
console.log('done')
|
||||
ctrl.close()
|
||||
return
|
||||
}
|
||||
|
||||
ctrl.enqueue('data: ')
|
||||
ctrl.enqueue(JSON.stringify(item.value))
|
||||
ctrl.enqueue('\r\n\r\n')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
ctrl.close()
|
||||
// Get the output as a stream
|
||||
const output = (await session.run(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
mode: 'openaicompatible', // Mode for the inference API host. (default: 'ollama')
|
||||
stream: true,
|
||||
}
|
||||
},
|
||||
})
|
||||
)) as AsyncGenerator<any>
|
||||
|
||||
return new Response(body.pipeThrough(new TextEncoderStream()), {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
},
|
||||
})
|
||||
})
|
||||
const body = new ReadableStream({
|
||||
async pull(ctrl) {
|
||||
try {
|
||||
const item = await output.next()
|
||||
|
||||
if (item.done) {
|
||||
console.log('done')
|
||||
ctrl.close()
|
||||
return
|
||||
}
|
||||
|
||||
ctrl.enqueue('data: ')
|
||||
ctrl.enqueue(JSON.stringify(item.value))
|
||||
ctrl.enqueue('\r\n\r\n')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
ctrl.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(body.pipeThrough(new TextEncoderStream()), {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
},
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
Run locally:
|
||||
@@ -61,7 +67,7 @@ Deno.serve(async (req: Request) => {
|
||||
supabase functions serve --env-file supabase/functions/.env
|
||||
|
||||
curl --get "http://localhost:54321/functions/v1/llamafile-stream" \
|
||||
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
|
||||
--data-urlencode "prompt=Who are you?"
|
||||
--data-urlencode "prompt=Who are you?" \
|
||||
-H "apikey: <SUPABASE_PUBLISHABLE_KEY>"
|
||||
|
||||
*/
|
||||
@@ -1,35 +1,41 @@
|
||||
// https://github.com/Mozilla-Ocho/llamafile?tab=readme-ov-file#quickstart
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
|
||||
const session = new Supabase.ai.Session('LLaMA_CPP')
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
const params = new URL(req.url).searchParams
|
||||
const prompt = params.get('prompt') ?? ''
|
||||
// Called with a publishable key on the `apikey` header. Deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'publishable' }, async (req) => {
|
||||
const params = new URL(req.url).searchParams
|
||||
const prompt = params.get('prompt') ?? ''
|
||||
|
||||
// Get the output as a stream
|
||||
const output = await session.run(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
mode: 'openaicompatible', // Mode for the inference API host. (default: 'ollama')
|
||||
stream: false,
|
||||
}
|
||||
)
|
||||
// Get the output as a stream
|
||||
const output = await session.run(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
mode: 'openaicompatible', // Mode for the inference API host. (default: 'ollama')
|
||||
stream: false,
|
||||
}
|
||||
)
|
||||
|
||||
console.log('done')
|
||||
return Response.json(output)
|
||||
})
|
||||
console.log('done')
|
||||
return Response.json(output)
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
Run locally:
|
||||
@@ -37,7 +43,7 @@ Deno.serve(async (req: Request) => {
|
||||
supabase functions serve --env-file supabase/functions/.env
|
||||
|
||||
curl --get "http://localhost:54321/functions/v1/llamafile" \
|
||||
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
|
||||
--data-urlencode "prompt=Who are you?"
|
||||
--data-urlencode "prompt=Who are you?" \
|
||||
-H "apikey: <SUPABASE_PUBLISHABLE_KEY>"
|
||||
|
||||
*/
|
||||
@@ -3,71 +3,75 @@
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import OpenAI from 'https://deno.land/x/openai@v4.53.2/mod.ts'
|
||||
import OpenAI from 'npm:openai@^6'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
console.log('Hello from openai-sdk compatible!')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const client = new OpenAI()
|
||||
const { prompt } = await req.json()
|
||||
const stream = true
|
||||
// Called with a publishable key on the `apikey` header. Deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'publishable' }, async (req) => {
|
||||
const client = new OpenAI()
|
||||
const { prompt } = await req.json()
|
||||
const stream = true
|
||||
|
||||
const chatCompletion = await client.chat.completions.create({
|
||||
model: 'LLaMA_CPP',
|
||||
stream,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (stream) {
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'text/event-stream',
|
||||
Connection: 'keep-alive',
|
||||
const chatCompletion = await client.chat.completions.create({
|
||||
model: 'LLaMA_CPP',
|
||||
stream,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Create a stream
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder()
|
||||
if (stream) {
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'text/event-stream',
|
||||
Connection: 'keep-alive',
|
||||
})
|
||||
|
||||
try {
|
||||
for await (const part of chatCompletion) {
|
||||
controller.enqueue(encoder.encode(part.choices[0]?.delta?.content || ''))
|
||||
// Create a stream
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
try {
|
||||
for await (const part of chatCompletion) {
|
||||
controller.enqueue(encoder.encode(part.choices[0]?.delta?.content || ''))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Stream error:', err)
|
||||
} finally {
|
||||
controller.close()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Stream error:', err)
|
||||
} finally {
|
||||
controller.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// Return the stream to the user
|
||||
return new Response(stream, {
|
||||
headers,
|
||||
})
|
||||
}
|
||||
// Return the stream to the user
|
||||
return new Response(stream, {
|
||||
headers,
|
||||
})
|
||||
}
|
||||
|
||||
console.log(chatCompletion)
|
||||
console.log(chatCompletion)
|
||||
|
||||
return Response.json(chatCompletion)
|
||||
})
|
||||
return Response.json(chatCompletion)
|
||||
}),
|
||||
}
|
||||
|
||||
/* To invoke locally:
|
||||
|
||||
supabase functions serve --env-file supabase/functions/.env
|
||||
|
||||
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/openai-sdk' \
|
||||
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
|
||||
--header 'apikey: <SUPABASE_PUBLISHABLE_KEY>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"prompt":"Who are you?"}'
|
||||
|
||||
|
||||
@@ -39,40 +39,77 @@ public = true
|
||||
objects_path = "./buckets/images"
|
||||
|
||||
[functions.auth-hook-react-email-resend]
|
||||
verify_jwt = false
|
||||
[functions.background-upload-storage]
|
||||
verify_jwt = false
|
||||
[functions.browser-with-cors]
|
||||
verify_jwt = true
|
||||
[functions.cloudflare-turnstile]
|
||||
verify_jwt = false
|
||||
[functions.custom-jwt-validation]
|
||||
verify_jwt = false
|
||||
[functions.connect-supabase]
|
||||
[functions.discord-bot]
|
||||
[functions.elevenlabs-speech-to-text]
|
||||
verify_jwt = false
|
||||
[functions.elevenlabs-text-to-speech]
|
||||
verify_jwt = true
|
||||
[functions.file-upload-storage]
|
||||
[functions.get-tshirt-competition]
|
||||
verify_jwt = false
|
||||
[functions.github-action-deploy]
|
||||
verify_jwt = true
|
||||
[functions.huggingface-image-captioning]
|
||||
verify_jwt = false
|
||||
[functions.image-manipulation]
|
||||
verify_jwt = true
|
||||
[functions.location]
|
||||
verify_jwt = true
|
||||
[functions.oak-server]
|
||||
[functions.og-image-with-storage-cdn]
|
||||
verify_jwt = false
|
||||
[functions.openai]
|
||||
verify_jwt = true
|
||||
[functions.openai-image-generation]
|
||||
verify_jwt = false
|
||||
[functions.opengraph]
|
||||
verify_jwt = true
|
||||
[functions.postgres-on-the-edge]
|
||||
[functions.puppeteer]
|
||||
verify_jwt = true
|
||||
[functions.simple-mcp-server]
|
||||
verify_jwt = false
|
||||
entrypoint = "./functions/mcp/simple-mcp-server/index.ts"
|
||||
[functions.read-storage]
|
||||
[functions.restful-tasks]
|
||||
[functions.select-from-table-with-auth-rls]
|
||||
[functions.send-email-resend]
|
||||
verify_jwt = false
|
||||
[functions.send-email-smtp]
|
||||
verify_jwt = true
|
||||
[functions.sentry]
|
||||
verify_jwt = false
|
||||
[functions.sentryfied]
|
||||
verify_jwt = true
|
||||
[functions.slack-bot-mention]
|
||||
verify_jwt = false
|
||||
[functions.streams]
|
||||
verify_jwt = true
|
||||
[functions.stripe-webhooks]
|
||||
verify_jwt = false
|
||||
[functions.telegram-bot]
|
||||
verify_jwt = false
|
||||
[functions.tweet-to-image]
|
||||
verify_jwt = false
|
||||
[functions.upstash-redis-counter]
|
||||
verify_jwt = true
|
||||
[functions.upstash-redis-ratelimit]
|
||||
verify_jwt = true
|
||||
|
||||
[functions.kysely-postgres]
|
||||
verify_jwt = true
|
||||
import_map = "./functions/import_map.json"
|
||||
|
||||
[functions.wasm-modules]
|
||||
verify_jwt = true
|
||||
static_files = [ "./functions/wasm-modules/add-wasm/pkg/*.wasm"]
|
||||
+2
-2
@@ -7,8 +7,8 @@ import {
|
||||
Link,
|
||||
Preview,
|
||||
Text,
|
||||
} from 'npm:@react-email/components@0.0.22'
|
||||
import * as React from 'npm:react@18.3.1'
|
||||
} from 'npm:@react-email/components@^1'
|
||||
import * as React from 'npm:react@^19'
|
||||
|
||||
interface MagicLinkEmailProps {
|
||||
supabase_url: string
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ import {
|
||||
Row,
|
||||
Section,
|
||||
Text,
|
||||
} from 'npm:@react-email/components@0.0.22'
|
||||
import * as React from 'npm:react@18.3.1'
|
||||
} from 'npm:@react-email/components@^1'
|
||||
import * as React from 'npm:react@^19'
|
||||
|
||||
interface SignUpEmailProps {
|
||||
username: string
|
||||
|
||||
@@ -1,102 +1,106 @@
|
||||
import React from 'npm:react@18.3.1'
|
||||
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
|
||||
import { Resend } from 'npm:resend@4.0.0'
|
||||
import { renderAsync } from 'npm:@react-email/components@0.0.22'
|
||||
import { Webhook } from 'npm:standardwebhooks@^1'
|
||||
import { render } from 'npm:@react-email/render@^2'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import React from 'npm:react@^19'
|
||||
import { Resend } from 'npm:resend@^6'
|
||||
|
||||
import { MagicLinkEmail } from './_templates/magic-link.tsx'
|
||||
import { SignUpEmail } from './_templates/sign-up.tsx'
|
||||
|
||||
const resend = new Resend(Deno.env.get('RESEND_API_KEY') as string)
|
||||
const hookSecret = (Deno.env.get('SEND_EMAIL_HOOK_SECRET') as string).replace('v1,whsec_', '')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method !== 'POST') {
|
||||
return new Response('not allowed', { status: 400 })
|
||||
}
|
||||
// Called by Supabase Auth as a webhook (verified via standardwebhooks
|
||||
// signature below), so deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req, _ctx) => {
|
||||
if (req.method !== 'POST') {
|
||||
return Response.json({ error: 'not allowed' }, { status: 400 })
|
||||
}
|
||||
|
||||
const payload = await req.text()
|
||||
const headers = Object.fromEntries(req.headers)
|
||||
const wh = new Webhook(hookSecret)
|
||||
try {
|
||||
const {
|
||||
user,
|
||||
email_data: { token, token_hash, redirect_to, email_action_type },
|
||||
} = wh.verify(payload, headers) as {
|
||||
user: {
|
||||
email: string
|
||||
user_metadata: {
|
||||
username: string
|
||||
lang: string
|
||||
const payload = await req.text()
|
||||
const headers = Object.fromEntries(req.headers)
|
||||
const wh = new Webhook(hookSecret)
|
||||
try {
|
||||
const {
|
||||
user,
|
||||
email_data: { token, token_hash, redirect_to, email_action_type },
|
||||
} = wh.verify(payload, headers) as {
|
||||
user: {
|
||||
email: string
|
||||
user_metadata: {
|
||||
username: string
|
||||
lang: string
|
||||
}
|
||||
}
|
||||
email_data: {
|
||||
token: string
|
||||
token_hash: string
|
||||
redirect_to: string
|
||||
email_action_type: string
|
||||
site_url: string
|
||||
token_new: string
|
||||
token_hash_new: string
|
||||
}
|
||||
}
|
||||
email_data: {
|
||||
token: string
|
||||
token_hash: string
|
||||
redirect_to: string
|
||||
email_action_type: string
|
||||
site_url: string
|
||||
token_new: string
|
||||
token_hash_new: string
|
||||
|
||||
let html: string
|
||||
|
||||
if (email_action_type === 'signup') {
|
||||
html = await render(
|
||||
React.createElement(SignUpEmail, {
|
||||
username: user['user_metadata'].username,
|
||||
lang: user['user_metadata'].lang,
|
||||
supabase_url: Deno.env.get('SUPABASE_URL') ?? '',
|
||||
token,
|
||||
token_hash,
|
||||
redirect_to,
|
||||
email_action_type,
|
||||
})
|
||||
)
|
||||
} else if (email_action_type == 'login') {
|
||||
html = await render(
|
||||
React.createElement(MagicLinkEmail, {
|
||||
supabase_url: Deno.env.get('SUPABASE_URL') ?? '',
|
||||
token,
|
||||
token_hash,
|
||||
redirect_to,
|
||||
email_action_type,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
// TODO implement reset_password
|
||||
throw new Error('Reset Password not implemented')
|
||||
}
|
||||
}
|
||||
|
||||
let html: string
|
||||
const { error } = await resend.emails.send({
|
||||
from: 'welcome <onboarding@resend.dev>',
|
||||
to: [user.email],
|
||||
subject: 'Supa Custom MagicLink!',
|
||||
html,
|
||||
})
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
const httpCode =
|
||||
typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined
|
||||
const message = error instanceof Error ? error.message : 'Unauthorized'
|
||||
|
||||
if (email_action_type === 'signup') {
|
||||
html = await renderAsync(
|
||||
React.createElement(SignUpEmail, {
|
||||
username: user['user_metadata'].username,
|
||||
lang: user['user_metadata'].lang,
|
||||
supabase_url: Deno.env.get('SUPABASE_URL') ?? '',
|
||||
token,
|
||||
token_hash,
|
||||
redirect_to,
|
||||
email_action_type,
|
||||
})
|
||||
)
|
||||
} else if (email_action_type == 'login') {
|
||||
html = await renderAsync(
|
||||
React.createElement(MagicLinkEmail, {
|
||||
supabase_url: Deno.env.get('SUPABASE_URL') ?? '',
|
||||
token,
|
||||
token_hash,
|
||||
redirect_to,
|
||||
email_action_type,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
// TODO implement reset_password
|
||||
throw new Error('Reset Password not implemented')
|
||||
}
|
||||
|
||||
const { error } = await resend.emails.send({
|
||||
from: 'welcome <onboarding@resend.dev>',
|
||||
to: [user.email],
|
||||
subject: 'Supa Custom MagicLink!',
|
||||
html,
|
||||
})
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
http_code: error.code,
|
||||
message: error.message,
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
http_code: httpCode,
|
||||
message,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const responseHeaders = new Headers()
|
||||
responseHeaders.set('Content-Type', 'application/json')
|
||||
return new Response(JSON.stringify({}), {
|
||||
status: 200,
|
||||
headers: responseHeaders,
|
||||
})
|
||||
})
|
||||
return Response.json({}, { status: 200 })
|
||||
}),
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import { createClient } from 'npm:supabase-js@2'
|
||||
import OpenAI from 'https://deno.land/x/openai@v4.68.2/mod.ts'
|
||||
import OpenAI from 'npm:openai@^6'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import type { SupabaseClient } from 'npm:@supabase/supabase-js@^2'
|
||||
|
||||
const client = new OpenAI({ apiKey: Deno.env.get('OPENAI_API_KEY')! })
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
|
||||
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default']!)
|
||||
|
||||
type StorageFileApi = ReturnType<typeof supabase.storage.from>
|
||||
type StorageFileApi = ReturnType<SupabaseClient['storage']['from']>
|
||||
type StorageUploadPromise = ReturnType<StorageFileApi['upload']>
|
||||
|
||||
class MyBackgroundTaskEvent extends Event {
|
||||
@@ -23,52 +21,49 @@ globalThis.addEventListener('myBackgroundTask', async (event) => {
|
||||
console.log({ data, error })
|
||||
})
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const url = new URL(req.url)
|
||||
const params = new URLSearchParams(url.search)
|
||||
const text = params.get('text')
|
||||
// Deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => {
|
||||
const url = new URL(req.url)
|
||||
const params = new URLSearchParams(url.search)
|
||||
const text = params.get('text')
|
||||
|
||||
if (!text) {
|
||||
return new Response(JSON.stringify({ error: 'Text parameter is required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await client.audio.speech.create({
|
||||
model: 'tts-1',
|
||||
voice: 'nova',
|
||||
input: text,
|
||||
})
|
||||
|
||||
const stream = response.body
|
||||
if (!stream) {
|
||||
throw new Error('No stream')
|
||||
if (!text) {
|
||||
return Response.json({ error: 'Text parameter is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Branch stream to Supabase Storage
|
||||
const [browserStream, storageStream] = stream.tee()
|
||||
|
||||
// Upload to Supabase Storage
|
||||
const storageUploadPromise = supabase.storage
|
||||
.from('videos')
|
||||
.upload(`audio-stream_${Date.now()}.mp3`, storageStream, {
|
||||
contentType: 'audio/mp3',
|
||||
try {
|
||||
const response = await client.audio.speech.create({
|
||||
model: 'tts-1',
|
||||
voice: 'nova',
|
||||
input: text,
|
||||
})
|
||||
const event = new MyBackgroundTaskEvent(storageUploadPromise)
|
||||
globalThis.dispatchEvent(event)
|
||||
|
||||
return new Response(browserStream, {
|
||||
headers: {
|
||||
'Content-Type': 'audio/mp3',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('error', { error })
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
})
|
||||
const stream = response.body
|
||||
if (!stream) {
|
||||
throw new Error('No stream')
|
||||
}
|
||||
|
||||
// Branch stream to Supabase Storage
|
||||
const [browserStream, storageStream] = stream.tee()
|
||||
|
||||
// Upload to Supabase Storage
|
||||
const storageUploadPromise = ctx.supabaseAdmin.storage
|
||||
.from('videos')
|
||||
.upload(`audio-stream_${Date.now()}.mp3`, storageStream, {
|
||||
contentType: 'audio/mp3',
|
||||
})
|
||||
const event = new MyBackgroundTaskEvent(storageUploadPromise)
|
||||
globalThis.dispatchEvent(event)
|
||||
|
||||
return new Response(browserStream, {
|
||||
headers: {
|
||||
'Content-Type': 'audio/mp3',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('error', { error })
|
||||
return Response.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -2,40 +2,29 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
// For @supabase/supabase-js v2.95.0+, import CORS headers directly from the SDK:
|
||||
import { corsHeaders } from 'jsr:@supabase/supabase-js@2/cors'
|
||||
|
||||
// For older versions, use a shared cors.ts file:
|
||||
// import { corsHeaders } from '../_shared/cors.ts'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
console.log(`Function "browser-with-cors" up and running!`)
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// This is needed if you're planning to invoke your function from a browser.
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
// withSupabase handles CORS automatically.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, _ctx) => {
|
||||
try {
|
||||
const { name } = await req.json()
|
||||
const data = {
|
||||
message: `Hello ${name}!`,
|
||||
}
|
||||
|
||||
try {
|
||||
const { name } = await req.json()
|
||||
const data = {
|
||||
message: `Hello ${name}!`,
|
||||
return Response.json(data)
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 400 })
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
||||
// To invoke:
|
||||
// curl -i --location --request POST 'http://localhost:54321/functions/v1/browser-with-cors' \
|
||||
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \
|
||||
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>' \
|
||||
// --header 'Content-Type: application/json' \
|
||||
// --data '{"name":"Functions"}'
|
||||
@@ -2,7 +2,7 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { corsHeaders } from '../_shared/cors.ts'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
console.log(`Function "cloudflare-turnstile" up and running!`)
|
||||
|
||||
@@ -10,51 +10,43 @@ function ips(req: Request) {
|
||||
return req.headers.get('x-forwarded-for')?.split(/\s*,\s*/)
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// This is needed if you're planning to invoke your function from a browser.
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
// Public endpoint, so deploy with verify_jwt = false.
|
||||
// withSupabase handles CORS automatically.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req, _ctx) => {
|
||||
try {
|
||||
const { token } = await req.json()
|
||||
if (!token) throw new Error('Missing token!')
|
||||
|
||||
try {
|
||||
const { token } = await req.json()
|
||||
if (!token) throw new Error('Missing token!')
|
||||
const clientIps = ips(req) || ['']
|
||||
|
||||
const clientIps = ips(req) || ['']
|
||||
// Validate the token by calling the
|
||||
// "/siteverify" API endpoint.
|
||||
const formData = new FormData()
|
||||
formData.append('secret', Deno.env.get('CLOUDFLARE_TURNSTILE_SECRET_KEY') ?? '')
|
||||
formData.append('response', token)
|
||||
formData.append('remoteip', clientIps[0])
|
||||
|
||||
// Validate the token by calling the
|
||||
// "/siteverify" API endpoint.
|
||||
const formData = new FormData()
|
||||
formData.append('secret', Deno.env.get('CLOUDFLARE_TURNSTILE_SECRET_KEY') ?? '')
|
||||
formData.append('response', token)
|
||||
formData.append('remoteip', clientIps[0])
|
||||
|
||||
const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
|
||||
const result = await fetch(url, {
|
||||
body: formData,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const outcome = await result.json()
|
||||
console.log(outcome)
|
||||
if (outcome.success) {
|
||||
return new Response(JSON.stringify(outcome), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
|
||||
const result = await fetch(url, {
|
||||
body: formData,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
throw new Error('Turnstile validation failed!')
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
})
|
||||
const outcome = await result.json()
|
||||
console.log(outcome)
|
||||
if (outcome.success) {
|
||||
return Response.json(outcome)
|
||||
}
|
||||
|
||||
throw new Error('Turnstile validation failed!')
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 400 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
// To invoke:
|
||||
// curl -i --location --request POST 'http://localhost:54321/functions/v1/cloudflare-turnstile' \
|
||||
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
|
||||
// --header 'Content-Type: application/json' \
|
||||
// --data '{"token":"cf-turnstile-response"}'
|
||||
@@ -1,19 +1,25 @@
|
||||
// Using 'default' Supabase auth middleware
|
||||
// Change to a specific provider by importing like that:
|
||||
// import { AuthMiddleware } from "../_shared/jwt/clerk.ts";
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
import { AuthMiddleware } from '../_shared/jwt/default.ts'
|
||||
|
||||
interface reqPayload {
|
||||
name: string
|
||||
}
|
||||
|
||||
Deno.serve((r) =>
|
||||
AuthMiddleware(r, async (req) => {
|
||||
const { name }: reqPayload = await req.json()
|
||||
const data = {
|
||||
message: `Hello ${name} from foo!`,
|
||||
}
|
||||
// Auth is handled by the custom AuthMiddleware below, so deploy with
|
||||
// verify_jwt = false. withSupabase only handles CORS here.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, (req) =>
|
||||
AuthMiddleware(req, async (r) => {
|
||||
const { name }: reqPayload = await r.json()
|
||||
const data = {
|
||||
message: `Hello ${name} from foo!`,
|
||||
}
|
||||
|
||||
return Response.json(data)
|
||||
})
|
||||
)
|
||||
return Response.json(data)
|
||||
})
|
||||
),
|
||||
}
|
||||
@@ -4,38 +4,40 @@
|
||||
|
||||
// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
import { createClient } from 'npm:supabase-js@2'
|
||||
|
||||
import { Bot, webhookCallback } from 'npm:grammy@^1'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import type { SupabaseClient } from 'npm:@supabase/supabase-js@^2'
|
||||
import { ElevenLabsClient } from 'npm:elevenlabs@^1'
|
||||
|
||||
declare const EdgeRuntime: {
|
||||
waitUntil(promise: Promise<unknown>): void
|
||||
}
|
||||
|
||||
console.log(`Function "elevenlabs-scribe-bot" up and running!`)
|
||||
|
||||
import { ElevenLabsClient } from 'npm:elevenlabs@1.50.5'
|
||||
import { Bot, webhookCallback } from 'https://deno.land/x/grammy@v1.34.0/mod.ts'
|
||||
|
||||
const elevenLabsClient = new ElevenLabsClient({
|
||||
apiKey: Deno.env.get('ELEVENLABS_API_KEY') || '',
|
||||
})
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL') || '',
|
||||
SUPABASE_SECRET_KEYS['default'] || ''
|
||||
)
|
||||
|
||||
async function scribe({
|
||||
bot,
|
||||
fileURL,
|
||||
fileType,
|
||||
duration,
|
||||
chatId,
|
||||
messageId,
|
||||
username,
|
||||
supabaseAdmin,
|
||||
}: {
|
||||
bot: Bot
|
||||
fileURL: string
|
||||
fileType: string
|
||||
duration: number
|
||||
chatId: number
|
||||
messageId: number
|
||||
username: string
|
||||
supabaseAdmin: SupabaseClient
|
||||
}) {
|
||||
let transcript: string | null = null
|
||||
let languageCode: string | null = null
|
||||
@@ -60,7 +62,7 @@ async function scribe({
|
||||
reply_parameters: { message_id: messageId },
|
||||
})
|
||||
} catch (error) {
|
||||
errorMsg = error.message
|
||||
errorMsg = error instanceof Error ? error.message : 'Unknown error'
|
||||
console.log(errorMsg)
|
||||
await bot.api.sendMessage(chatId, 'Sorry, there was an error. Please try again.', {
|
||||
reply_parameters: { message_id: messageId },
|
||||
@@ -77,68 +79,77 @@ async function scribe({
|
||||
error: errorMsg,
|
||||
}
|
||||
console.log({ logLine })
|
||||
await supabase.from('transcription_logs').insert({ ...logLine, transcript })
|
||||
await supabaseAdmin.from('transcription_logs').insert({ ...logLine, transcript })
|
||||
}
|
||||
|
||||
// Use beforeunload event handler to be notified when function is about to shutdown
|
||||
addEventListener('beforeunload', (ev) => {
|
||||
console.log('Function will be shutdown due to', ev.detail?.reason)
|
||||
const reason = (ev as Event & { detail?: { reason?: string } }).detail?.reason
|
||||
console.log('Function will be shutdown due to', reason)
|
||||
|
||||
// save state or log the current progress
|
||||
})
|
||||
|
||||
const telegramBotToken = Deno.env.get('TELEGRAM_BOT_TOKEN')
|
||||
const bot = new Bot(telegramBotToken || '')
|
||||
const startMessage = `Welcome to the ElevenLabs Scribe Bot\\! I can transcribe speech in 80\\+ languages with super high accuracy\\!
|
||||
\nTry it out by sending or forwarding me a voice message, video, or audio file\\!
|
||||
\n[Learn more about Scribe](https://elevenlabs.io/speech-to-text) or [build your own bot](https://elevenlabs.io/docs/cookbooks/speech-to-text/telegram-bot)\\!
|
||||
`
|
||||
bot.command('start', (ctx) => ctx.reply(startMessage.trim(), { parse_mode: 'MarkdownV2' }))
|
||||
|
||||
bot.on([':voice', ':audio', ':video'], async (ctx) => {
|
||||
try {
|
||||
// console.log(ctx);
|
||||
const file = await ctx.getFile()
|
||||
const fileURL = `https://api.telegram.org/file/bot${telegramBotToken}/${file.file_path}`
|
||||
const fileMeta = ctx.message?.video ?? ctx.message?.voice ?? ctx.message?.audio
|
||||
// console.log({ fileURL, fileMeta });
|
||||
if (!fileMeta) {
|
||||
return ctx.reply('No video|audio|voice metadata found. Please try again.')
|
||||
// Deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
|
||||
const bot = new Bot(telegramBotToken || '')
|
||||
const supabaseAdmin = ctx.supabaseAdmin
|
||||
|
||||
bot.command('start', (ctx) => ctx.reply(startMessage.trim(), { parse_mode: 'MarkdownV2' }))
|
||||
bot.on([':voice', ':audio', ':video'], async (ctx) => {
|
||||
try {
|
||||
// console.log(ctx);
|
||||
const file = await ctx.getFile()
|
||||
const fileURL = `https://api.telegram.org/file/bot${telegramBotToken}/${file.file_path}`
|
||||
const fileMeta = ctx.message?.video ?? ctx.message?.voice ?? ctx.message?.audio
|
||||
// console.log({ fileURL, fileMeta });
|
||||
if (!fileMeta) {
|
||||
return ctx.reply('No video|audio|voice metadata found. Please try again.')
|
||||
}
|
||||
|
||||
// Run the transcription in the background.
|
||||
EdgeRuntime.waitUntil(
|
||||
scribe({
|
||||
bot,
|
||||
fileURL,
|
||||
fileType: fileMeta.mime_type!,
|
||||
duration: fileMeta.duration,
|
||||
chatId: ctx.chat.id,
|
||||
messageId: ctx.message?.message_id!,
|
||||
username: ctx.from?.username || '',
|
||||
supabaseAdmin,
|
||||
})
|
||||
)
|
||||
|
||||
// Reply to the user immediately to let them know we received their file.
|
||||
return ctx.reply('Received. Scribing...')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return ctx.reply(
|
||||
'Sorry, there was an error getting the file. Please try again with a smaller file!'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const handleUpdate = webhookCallback(bot, 'std/http')
|
||||
|
||||
try {
|
||||
const url = new URL(req.url)
|
||||
if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) {
|
||||
return new Response('not allowed', { status: 405 })
|
||||
}
|
||||
|
||||
return await handleUpdate(req)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return new Response('Internal Server Error', { status: 500 })
|
||||
}
|
||||
|
||||
// Run the transcription in the background.
|
||||
EdgeRuntime.waitUntil(
|
||||
scribe({
|
||||
fileURL,
|
||||
fileType: fileMeta.mime_type!,
|
||||
duration: fileMeta.duration,
|
||||
chatId: ctx.chat.id,
|
||||
messageId: ctx.message?.message_id!,
|
||||
username: ctx.from?.username || '',
|
||||
})
|
||||
)
|
||||
|
||||
// Reply to the user immediately to let them know we received their file.
|
||||
return ctx.reply('Received. Scribing...')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return ctx.reply(
|
||||
'Sorry, there was an error getting the file. Please try again with a smaller file!'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const handleUpdate = webhookCallback(bot, 'std/http')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const url = new URL(req.url)
|
||||
if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) {
|
||||
return new Response('not allowed', { status: 405 })
|
||||
}
|
||||
|
||||
return await handleUpdate(req)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
@@ -1,19 +1,26 @@
|
||||
// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
import { createClient } from 'npm:supabase-js@2'
|
||||
import { ElevenLabsClient } from 'npm:elevenlabs@1.52.0'
|
||||
import * as hash from 'npm:object-hash'
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default']!)
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import type { SupabaseClient } from 'npm:@supabase/supabase-js@^2'
|
||||
import { ElevenLabsClient } from 'npm:elevenlabs@^1'
|
||||
import * as hash from 'npm:object-hash@^3'
|
||||
|
||||
declare const EdgeRuntime: {
|
||||
waitUntil(promise: Promise<unknown>): void
|
||||
}
|
||||
|
||||
const client = new ElevenLabsClient({
|
||||
apiKey: Deno.env.get('ELEVENLABS_API_KEY'),
|
||||
})
|
||||
|
||||
// Upload audio to Supabase Storage in a background task
|
||||
async function uploadAudioToStorage(stream: ReadableStream, requestHash: string) {
|
||||
const { data, error } = await supabase.storage
|
||||
async function uploadAudioToStorage(
|
||||
supabaseAdmin: SupabaseClient,
|
||||
stream: ReadableStream,
|
||||
requestHash: string
|
||||
) {
|
||||
const { data, error } = await supabaseAdmin.storage
|
||||
.from('audio')
|
||||
.upload(`${requestHash}.mp3`, stream, {
|
||||
contentType: 'audio/mp3',
|
||||
@@ -22,68 +29,66 @@ async function uploadAudioToStorage(stream: ReadableStream, requestHash: string)
|
||||
console.log('Storage upload result', { data, error })
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// To secure your function for production, you can for example validate the request origin,
|
||||
// or append a user access token and validate it with Supabase Auth.
|
||||
console.log('Request origin', req.headers.get('host'))
|
||||
const url = new URL(req.url)
|
||||
const params = new URLSearchParams(url.search)
|
||||
const text = params.get('text')
|
||||
const voiceId = params.get('voiceId') ?? 'JBFqnCBsd6RMkjVDRZzb'
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
// You can add extra checks here, for example validating the request origin.
|
||||
console.log('Request origin', req.headers.get('host'))
|
||||
const url = new URL(req.url)
|
||||
const params = new URLSearchParams(url.search)
|
||||
const text = params.get('text')
|
||||
const voiceId = params.get('voiceId') ?? 'JBFqnCBsd6RMkjVDRZzb'
|
||||
|
||||
const requestHash = hash.MD5({ text, voiceId })
|
||||
console.log('Request hash', requestHash)
|
||||
const requestHash = hash.MD5({ text, voiceId })
|
||||
console.log('Request hash', requestHash)
|
||||
|
||||
// Check storage for existing audio file
|
||||
const { data } = await supabase.storage.from('audio').createSignedUrl(`${requestHash}.mp3`, 60)
|
||||
// Check storage for existing audio file
|
||||
const { data } = await ctx.supabaseAdmin.storage
|
||||
.from('audio')
|
||||
.createSignedUrl(`${requestHash}.mp3`, 60)
|
||||
|
||||
if (data) {
|
||||
console.log('Audio file found in storage', data)
|
||||
const storageRes = await fetch(data.signedUrl)
|
||||
if (storageRes.ok) return storageRes
|
||||
}
|
||||
if (data) {
|
||||
console.log('Audio file found in storage', data)
|
||||
const storageRes = await fetch(data.signedUrl)
|
||||
if (storageRes.ok) return storageRes
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
return new Response(JSON.stringify({ error: 'Text parameter is required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
if (!text) {
|
||||
return Response.json({ error: 'Text parameter is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('ElevenLabs API call')
|
||||
const response = await client.textToSpeech.convertAsStream(voiceId, {
|
||||
output_format: 'mp3_44100_128',
|
||||
model_id: 'eleven_multilingual_v2',
|
||||
text,
|
||||
})
|
||||
try {
|
||||
console.log('ElevenLabs API call')
|
||||
const response = await client.textToSpeech.convertAsStream(voiceId, {
|
||||
output_format: 'mp3_44100_128',
|
||||
model_id: 'eleven_multilingual_v2',
|
||||
text,
|
||||
})
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
for await (const chunk of response) {
|
||||
controller.enqueue(chunk)
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
for await (const chunk of response) {
|
||||
controller.enqueue(chunk)
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
// Branch stream to Supabase Storage
|
||||
const [browserStream, storageStream] = stream.tee()
|
||||
// Branch stream to Supabase Storage
|
||||
const [browserStream, storageStream] = stream.tee()
|
||||
|
||||
// Upload to Supabase Storage in the background
|
||||
EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash))
|
||||
// Upload to Supabase Storage in the background
|
||||
EdgeRuntime.waitUntil(uploadAudioToStorage(ctx.supabaseAdmin, storageStream, requestHash))
|
||||
|
||||
// Return the streaming response immediately
|
||||
return new Response(browserStream, {
|
||||
headers: {
|
||||
'Content-Type': 'audio/mpeg',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('error', { error })
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
})
|
||||
// Return the streaming response immediately
|
||||
return new Response(browserStream, {
|
||||
headers: {
|
||||
'Content-Type': 'audio/mpeg',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('error', { error })
|
||||
return Response.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -2,11 +2,7 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { createClient } from 'npm:supabase-js@2'
|
||||
// New approach (v2.95.0+)
|
||||
import { corsHeaders } from 'jsr:@supabase/supabase-js@2/cors'
|
||||
// For older versions:
|
||||
// import { corsHeaders } from '../_shared/cors.ts'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
console.log(`Function "get-tshirt-competition" up and running!`)
|
||||
|
||||
@@ -25,72 +21,54 @@ function turnEmailToCount(email: string): string {
|
||||
)}`
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// This is needed if you're planning to invoke your function from a browser.
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
// Public challenge endpoint, so deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase<any>({ auth: 'none' }, async (req, ctx) => {
|
||||
try {
|
||||
const url = new URL(req.url)
|
||||
const email = url.searchParams.get('email')
|
||||
const twitter = url.searchParams.get('twitter')
|
||||
const size = url.searchParams.get('size')
|
||||
const answer = url.searchParams.get('answer')
|
||||
|
||||
try {
|
||||
const url = new URL(req.url)
|
||||
const email = url.searchParams.get('email')
|
||||
const twitter = url.searchParams.get('twitter')
|
||||
const size = url.searchParams.get('size')
|
||||
const answer = url.searchParams.get('answer')
|
||||
|
||||
if (
|
||||
!/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email ?? '') ||
|
||||
!answer ||
|
||||
!twitter ||
|
||||
!size
|
||||
) {
|
||||
throw new Error(
|
||||
`Please provide valid 'email', 'twitter', 'size', and 'answer' params. HINT: https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts`
|
||||
)
|
||||
}
|
||||
|
||||
if (answer !== countEmailSegments(email!)) {
|
||||
throw new Error(
|
||||
`Sorry, that's wrong, please try again! HINT: https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts`
|
||||
)
|
||||
}
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
|
||||
const supabaseAdminClient = createClient(
|
||||
// Supabase API URL - env var exported by default when deployed.
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
// Supabase API SECRET KEY - env var exported by default when deployed.
|
||||
SUPABASE_SECRET_KEYS['default'] ?? ''
|
||||
)
|
||||
// Submit email to draw
|
||||
const { error } = await supabaseAdminClient.from('get-tshirt-competition-2').upsert(
|
||||
{
|
||||
email,
|
||||
twitter,
|
||||
size,
|
||||
},
|
||||
{ onConflict: 'email' }
|
||||
)
|
||||
if (error) {
|
||||
console.log(error)
|
||||
throw new Error(error.details)
|
||||
}
|
||||
|
||||
return new Response(
|
||||
`Thanks for playing! ${turnEmailToCount(email!)} has been added to the draw \\o/`,
|
||||
{
|
||||
headers: { ...corsHeaders, 'Content-Type': 'text/plain' },
|
||||
status: 200,
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email ?? '') || !answer || !twitter || !size) {
|
||||
throw new Error(
|
||||
`Please provide valid 'email', 'twitter', 'size', and 'answer' params. HINT: https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts`
|
||||
)
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (answer !== countEmailSegments(email!)) {
|
||||
throw new Error(
|
||||
`Sorry, that's wrong, please try again! HINT: https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts`
|
||||
)
|
||||
}
|
||||
|
||||
// Submit email to draw
|
||||
const { error } = await ctx.supabaseAdmin.from('get-tshirt-competition-2').upsert(
|
||||
{
|
||||
email,
|
||||
twitter,
|
||||
size,
|
||||
},
|
||||
{ onConflict: 'email' }
|
||||
)
|
||||
if (error) {
|
||||
console.log(error)
|
||||
throw new Error(error.details)
|
||||
}
|
||||
|
||||
return new Response(
|
||||
`Thanks for playing! ${turnEmailToCount(email!)} has been added to the draw \\o/`,
|
||||
{
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
status: 200,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 400 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
// To invoke:
|
||||
// curl -i --location --request GET 'http://localhost:54321/functions/v1/get-tshirt-competition?email=testr@test.de&twitter=thorwebdev&size=2XL&answer=20'
|
||||
@@ -2,16 +2,21 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
console.log('Hello from Functions!')
|
||||
|
||||
Deno.serve((_req) => {
|
||||
const data = {
|
||||
message: `I was deployed via GitHub Actions!`,
|
||||
}
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (_req, _ctx) => {
|
||||
const data = {
|
||||
message: `I was deployed via GitHub Actions!`,
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
})
|
||||
return Response.json(data)
|
||||
}),
|
||||
}
|
||||
|
||||
// To invoke: http://localhost:54321/functions/v1/github-action-deploy
|
||||
// To invoke:
|
||||
// curl -i --location --request GET 'http://localhost:54321/functions/v1/github-action-deploy' \
|
||||
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>'
|
||||
@@ -1,5 +1,6 @@
|
||||
import { HfInference } from 'https://esm.sh/@huggingface/inference@2.3.2'
|
||||
import { createClient } from 'npm:supabase-js@2'
|
||||
import { HfInference } from 'npm:@huggingface/inference@^4'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
import { Database } from './types.ts'
|
||||
|
||||
console.log('Hello from `huggingface-image-captioning` function!')
|
||||
@@ -15,35 +16,31 @@ interface WebhookPayload {
|
||||
old_record: null | SoRecord
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
const soRecord = payload.record
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
const supabaseAdminClient = createClient<Database>(
|
||||
// Supabase API URL - env var exported by default when deployed.
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
// Supabase API SECRET KEY - env var exported by default when deployed.
|
||||
SUPABASE_SECRET_KEYS['default'] ?? ''
|
||||
)
|
||||
// Deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase<Database>({ auth: 'secret' }, async (req, ctx) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
const soRecord = payload.record
|
||||
|
||||
// Construct image url from storage
|
||||
const { data, error } = await supabaseAdminClient.storage
|
||||
.from(soRecord.bucket_id!)
|
||||
.createSignedUrl(soRecord.path_tokens!.join('/'), 60)
|
||||
if (error) throw error
|
||||
const { signedUrl } = data
|
||||
// Construct image url from storage
|
||||
const { data, error } = await ctx.supabaseAdmin.storage
|
||||
.from(soRecord.bucket_id!)
|
||||
.createSignedUrl(soRecord.path_tokens!.join('/'), 60)
|
||||
if (error) throw error
|
||||
const { signedUrl } = data
|
||||
|
||||
// Run image captioning with Huggingface
|
||||
const imgDesc = await hf.imageToText({
|
||||
data: await (await fetch(signedUrl)).blob(),
|
||||
model: 'nlpconnect/vit-gpt2-image-captioning',
|
||||
})
|
||||
// Run image captioning with Huggingface
|
||||
const imgDesc = await hf.imageToText({
|
||||
data: await (await fetch(signedUrl)).blob(),
|
||||
model: 'nlpconnect/vit-gpt2-image-captioning',
|
||||
})
|
||||
|
||||
// Store image caption in Database table
|
||||
await supabaseAdminClient
|
||||
.from('image_caption')
|
||||
.insert({ id: soRecord.id!, caption: imgDesc.generated_text })
|
||||
.throwOnError()
|
||||
// Store image caption in Database table
|
||||
await ctx.supabaseAdmin
|
||||
.from('image_caption')
|
||||
.insert({ id: soRecord.id!, caption: imgDesc.generated_text })
|
||||
.throwOnError()
|
||||
|
||||
return new Response('ok')
|
||||
})
|
||||
return Response.json({ status: 'ok' })
|
||||
}),
|
||||
}
|
||||
@@ -1,28 +1,34 @@
|
||||
// This is an example showing how to use Magick WASM to do image manipulations in Edge Functions.
|
||||
//
|
||||
import {
|
||||
ImageMagick,
|
||||
initializeImageMagick,
|
||||
MagickFormat,
|
||||
} from 'npm:@imagemagick/magick-wasm@0.0.30'
|
||||
import { ImageMagick, initializeImageMagick, MagickFormat } from 'npm:@imagemagick/magick-wasm@^0'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const wasmBytes = await Deno.readFile(
|
||||
new URL('magick.wasm', import.meta.resolve('npm:@imagemagick/magick-wasm@0.0.30'))
|
||||
new URL('magick.wasm', import.meta.resolve('npm:@imagemagick/magick-wasm@^0'))
|
||||
)
|
||||
await initializeImageMagick(wasmBytes)
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const formData = await req.formData()
|
||||
const content = await formData.get('file').bytes()
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, _ctx) => {
|
||||
const formData = await req.formData()
|
||||
const file = formData.get('file')
|
||||
if (!(file instanceof Blob)) {
|
||||
return Response.json({ error: 'file is required' }, { status: 400 })
|
||||
}
|
||||
const content = await file.bytes()
|
||||
|
||||
let result = ImageMagick.read(content, (img): Uint8Array => {
|
||||
// resize the image
|
||||
img.resize(500, 300)
|
||||
// add a blur of 60x5
|
||||
img.blur(60, 5)
|
||||
let result = ImageMagick.read(content, (img): Uint8Array => {
|
||||
// resize the image
|
||||
img.resize(500, 300)
|
||||
// add a blur of 60x5
|
||||
img.blur(60, 5)
|
||||
|
||||
return img.write((data) => data)
|
||||
})
|
||||
return img.write((data) => data)
|
||||
})
|
||||
|
||||
return new Response(result, { headers: { 'Content-Type': 'image/png' } })
|
||||
})
|
||||
return new Response(Uint8Array.from(result), {
|
||||
headers: { 'Content-Type': 'image/png' },
|
||||
})
|
||||
}),
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { Pool } from 'postgres'
|
||||
import {
|
||||
Generated,
|
||||
Kysely,
|
||||
@@ -10,6 +9,9 @@ import {
|
||||
PostgresIntrospector,
|
||||
PostgresQueryCompiler,
|
||||
} from 'kysely'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import { Pool } from 'postgres'
|
||||
|
||||
import { PostgresDriver } from './DenoPostgresDriver.ts'
|
||||
|
||||
console.log(`Function "kysely-postgres" up and running!`)
|
||||
@@ -58,32 +60,42 @@ const db = new Kysely<Database>({
|
||||
},
|
||||
})
|
||||
|
||||
Deno.serve(async (_req) => {
|
||||
try {
|
||||
// Run a query
|
||||
const animals = await db.selectFrom('animals').select(['id', 'animal', 'created_at']).execute()
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (_req, _ctx) => {
|
||||
try {
|
||||
// Run a query
|
||||
const animals = await db
|
||||
.selectFrom('animals')
|
||||
.select(['id', 'animal', 'created_at'])
|
||||
.execute()
|
||||
|
||||
// Neat, it's properly typed \o/
|
||||
console.log(animals[0].created_at.getFullYear())
|
||||
// Neat, it's properly typed \o/
|
||||
if (animals[0]) {
|
||||
console.log(animals[0].created_at.getFullYear())
|
||||
}
|
||||
|
||||
// Encode the result as pretty printed JSON
|
||||
const body = JSON.stringify(
|
||||
animals,
|
||||
(key, value) => (typeof value === 'bigint' ? value.toString() : value),
|
||||
2
|
||||
)
|
||||
// Encode the result as pretty printed JSON
|
||||
const body = JSON.stringify(
|
||||
animals,
|
||||
(key, value) => (typeof value === 'bigint' ? value.toString() : value),
|
||||
2
|
||||
)
|
||||
|
||||
// Return the response with the correct content type header
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return new Response(String(err?.message ?? err), { status: 500 })
|
||||
}
|
||||
})
|
||||
// Return the response with the correct content type header
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return new Response('Internal Server Error', { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
// To invoke: navigate to http://localhost:54321/functions/v1/kysely-postgres
|
||||
// To invoke:
|
||||
// curl -i --location --request GET 'http://localhost:54321/functions/v1/kysely-postgres' \
|
||||
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>'
|
||||
@@ -2,27 +2,30 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
function ips(req: Request) {
|
||||
return req.headers.get('x-forwarded-for')?.split(/\s*,\s*/)
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const clientIps = ips(req) || ['']
|
||||
const res = await fetch(
|
||||
`https://ipinfo.io/${clientIps[0]}?token=${Deno.env.get('IPINFO_TOKEN')}`,
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
if (res.ok) {
|
||||
const { city, country } = await res.json()
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, _ctx) => {
|
||||
const clientIps = ips(req) || ['']
|
||||
const res = await fetch(
|
||||
`https://ipinfo.io/${clientIps[0]}?token=${Deno.env.get('IPINFO_TOKEN')}`,
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
if (res.ok) {
|
||||
const { city, country } = await res.json()
|
||||
|
||||
return new Response(JSON.stringify(`You're accessing from ${city}, ${country}`), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
} else {
|
||||
return new Response(await res.text(), {
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
})
|
||||
return Response.json(`You're accessing from ${city}, ${country}`)
|
||||
} else {
|
||||
return new Response(await res.text(), {
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { StreamableHTTPTransport } from '@hono/mcp'
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { Hono } from 'hono'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import { z } from 'zod'
|
||||
|
||||
// Create Hono app
|
||||
@@ -35,4 +36,7 @@ app.all('/', async (c) => {
|
||||
return transport.handleRequest(c)
|
||||
})
|
||||
|
||||
Deno.serve(app.fetch)
|
||||
// Public endpoint, so deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, app.fetch),
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'https://esm.sh/react@18.2.0?deno-std=0.177.0'
|
||||
import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts'
|
||||
import { createClient } from 'jsr:@supabase/supabase-js@2'
|
||||
import { corsHeaders } from '../_shared/cors.ts'
|
||||
import { ImageResponse } from 'npm:@vercel/og@^0'
|
||||
import React from 'npm:react@19'
|
||||
|
||||
const STORAGE_URL = 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw6'
|
||||
const BACKGROUND_IMAGE_STD = `${STORAGE_URL}/lw6_ticket_regular.png`
|
||||
@@ -13,7 +11,7 @@ const SUPA_CHECKMARK_GOLD = `${STORAGE_URL}/supaverified_gold.png?v=3`
|
||||
const FONT_URL = `${STORAGE_URL}/CircularStd-Book.otf`
|
||||
const font = fetch(new URL(FONT_URL, import.meta.url)).then((res) => res.arrayBuffer())
|
||||
|
||||
export async function handler(req: Request) {
|
||||
export async function handler(req: Request, ctx) {
|
||||
const url = new URL(req.url)
|
||||
const ticketNumber = url.searchParams.get('ticketNumber')
|
||||
const username = url.searchParams.get('username') ?? url.searchParams.get('amp;username')
|
||||
@@ -21,10 +19,7 @@ export async function handler(req: Request) {
|
||||
const golden = url.searchParams.get('golden') ?? url.searchParams.get('amp;golden')
|
||||
|
||||
if (!username || !ticketNumber || !name) {
|
||||
return new Response(JSON.stringify({ error: 'missing params' }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
return Response.json({ error: 'missing params' }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -195,17 +190,8 @@ export async function handler(req: Request) {
|
||||
}
|
||||
)
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
|
||||
const supabaseAdminClient = createClient(
|
||||
// Supabase API URL - env var exported by default when deployed.
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
// Supabase API SECRET KEY - env var exported by default when deployed.
|
||||
SUPABASE_SECRET_KEYS['default'] ?? ''
|
||||
)
|
||||
|
||||
// Upload image to storage.
|
||||
const { error } = await supabaseAdminClient.storage
|
||||
const { error } = await ctx.supabaseAdmin.storage
|
||||
.from('images')
|
||||
.upload(`lw6/tickets/${username}.png`, generatedImage.body!, {
|
||||
contentType: 'image/png',
|
||||
@@ -216,9 +202,6 @@ export async function handler(req: Request) {
|
||||
|
||||
return await fetch(`${STORAGE_URL}/tickets/${username}.png?v=3`)
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
return Response.json({ error: error.message }, { status: 400 })
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,12 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
import { handler } from './handler.tsx'
|
||||
|
||||
console.log(`Function "og-image-with-storage-cdn" up and running!`)
|
||||
|
||||
Deno.serve(handler)
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'secret' }, handler),
|
||||
}
|
||||
@@ -2,142 +2,111 @@
|
||||
// - `generated-images` bucket already created.
|
||||
// - `OPENAI_API_KEY` environment variable set.
|
||||
|
||||
import { createClient } from 'npm:supabase-js@2'
|
||||
import OpenAI, { toFile } from 'jsr:@openai/openai@4.96.2'
|
||||
import OpenAI, { toFile } from 'jsr:@openai/openai@^6'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
// Configure COS headers for the function
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
}
|
||||
// Deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => {
|
||||
try {
|
||||
const { prompt, imageUrls }: { prompt: string; imageUrls: string[] } = await req.json()
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// Handle CORS preflight requests
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, {
|
||||
headers: corsHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const { prompt, imageUrls }: { prompt: string; imageUrls: string[] } = await req.json()
|
||||
|
||||
if (!prompt || !imageUrls || imageUrls.length === 0) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Prompt and at least one image URL are required',
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Create OpenAI client
|
||||
const openai = new OpenAI({
|
||||
apiKey: Deno.env.get('OPENAI_API_KEY'),
|
||||
})
|
||||
|
||||
// Get the images from the provided URLs
|
||||
const imagePromises = imageUrls.map(async (url: string) => {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image from ${url}`)
|
||||
if (!prompt || !imageUrls || imageUrls.length === 0) {
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Prompt and at least one image URL are required',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
return await response.blob()
|
||||
})
|
||||
|
||||
const imageBlobs = await Promise.all(imagePromises)
|
||||
|
||||
const images = await Promise.all(
|
||||
imageBlobs.map(
|
||||
async (blob, index) =>
|
||||
await toFile(blob, `image-${index}.png`, {
|
||||
type: blob.type || 'image/png',
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// Call OpenAI API using the client
|
||||
console.log('Calling OpenAI API with prompt:', prompt)
|
||||
const response = await openai.images.edit({
|
||||
model: 'gpt-image-1',
|
||||
image: images,
|
||||
prompt: prompt,
|
||||
})
|
||||
|
||||
if (!response.data || response.data.length === 0) {
|
||||
throw new Error('No image data received from OpenAI API')
|
||||
}
|
||||
|
||||
// Convert base64 to blob
|
||||
const base64Data = response.data[0].b64_json!
|
||||
const binaryData = atob(base64Data)
|
||||
const uint8Array = new Uint8Array(binaryData.length)
|
||||
for (let i = 0; i < binaryData.length; i++) {
|
||||
uint8Array[i] = binaryData.charCodeAt(i)
|
||||
}
|
||||
const generatedImageBlob = new Blob([uint8Array], { type: 'image/png' })
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
|
||||
// Upload the generated image to Supabase Storage
|
||||
const supabaseClient = createClient(
|
||||
Deno.env.get('SUPABASE_URL') || '',
|
||||
SUPABASE_SECRET_KEYS['default'] || ''
|
||||
)
|
||||
|
||||
// Create a unique identifier for this generation
|
||||
const timestamp = new Date().toISOString()
|
||||
const randomId = crypto.randomUUID()
|
||||
const outputImageName = `generated-${timestamp}-${randomId}.png`
|
||||
|
||||
const { error: uploadError } = await supabaseClient.storage
|
||||
.from('generated-images')
|
||||
.upload(outputImageName, generatedImageBlob, {
|
||||
contentType: 'image/png',
|
||||
upsert: true,
|
||||
// Create OpenAI client
|
||||
const openai = new OpenAI({
|
||||
apiKey: Deno.env.get('OPENAI_API_KEY'),
|
||||
})
|
||||
|
||||
if (uploadError) {
|
||||
console.error('Supabase Storage upload error:', uploadError)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Failed to upload generated image to storage',
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
// Get the images from the provided URLs
|
||||
const imagePromises = imageUrls.map(async (url: string) => {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image from ${url}`)
|
||||
}
|
||||
return await response.blob()
|
||||
})
|
||||
|
||||
const imageBlobs = await Promise.all(imagePromises)
|
||||
|
||||
const images = await Promise.all(
|
||||
imageBlobs.map(
|
||||
async (blob, index) =>
|
||||
await toFile(blob, `image-${index}.png`, {
|
||||
type: blob.type || 'image/png',
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Get the public URL for the uploaded image
|
||||
const {
|
||||
data: { publicUrl },
|
||||
} = supabaseClient.storage.from('generated-images').getPublicUrl(outputImageName)
|
||||
// Call OpenAI API using the client
|
||||
console.log('Calling OpenAI API with prompt:', prompt)
|
||||
const response = await openai.images.edit({
|
||||
model: 'gpt-image-1',
|
||||
image: images,
|
||||
prompt: prompt,
|
||||
})
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
if (!response.data || response.data.length === 0) {
|
||||
throw new Error('No image data received from OpenAI API')
|
||||
}
|
||||
|
||||
// Convert base64 to blob
|
||||
const base64Data = response.data[0].b64_json!
|
||||
const binaryData = atob(base64Data)
|
||||
const uint8Array = new Uint8Array(binaryData.length)
|
||||
for (let i = 0; i < binaryData.length; i++) {
|
||||
uint8Array[i] = binaryData.charCodeAt(i)
|
||||
}
|
||||
const generatedImageBlob = new Blob([uint8Array], { type: 'image/png' })
|
||||
|
||||
// Create a unique identifier for this generation
|
||||
const timestamp = new Date().toISOString()
|
||||
const randomId = crypto.randomUUID()
|
||||
const outputImageName = `generated-${timestamp}-${randomId}.png`
|
||||
|
||||
// Upload the generated image to Supabase Storage
|
||||
const { error: uploadError } = await ctx.supabaseAdmin.storage
|
||||
.from('generated-images')
|
||||
.upload(outputImageName, generatedImageBlob, {
|
||||
contentType: 'image/png',
|
||||
upsert: true,
|
||||
})
|
||||
|
||||
if (uploadError) {
|
||||
console.error('Supabase Storage upload error:', uploadError)
|
||||
return Response.json(
|
||||
{
|
||||
error: 'Failed to upload generated image to storage',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get the public URL for the uploaded image
|
||||
const {
|
||||
data: { publicUrl },
|
||||
} = ctx.supabaseAdmin.storage.from('generated-images').getPublicUrl(outputImageName)
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
generatedImageUrl: publicUrl,
|
||||
message: 'Image processed and stored successfully',
|
||||
}),
|
||||
{
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error processing image:', error)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: (error as Error)?.message || 'Unknown error occurred',
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error processing image:', error)
|
||||
return Response.json(
|
||||
{
|
||||
error: (error as Error)?.message || 'Unknown error occurred',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -1,23 +1,25 @@
|
||||
import 'https://deno.land/x/xhr@0.3.0/mod.ts'
|
||||
import { CreateCompletionRequest } from 'https://esm.sh/openai@3.1.0'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const { query } = await req.json()
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req) => {
|
||||
const { query } = await req.json()
|
||||
|
||||
const completionConfig: CreateCompletionRequest = {
|
||||
model: 'text-davinci-003',
|
||||
prompt: query,
|
||||
max_tokens: 256,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
}
|
||||
const completionConfig = {
|
||||
model: 'text-davinci-003',
|
||||
prompt: query,
|
||||
max_tokens: 256,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
}
|
||||
|
||||
return fetch('https://api.openai.com/v1/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(completionConfig),
|
||||
})
|
||||
})
|
||||
return fetch('https://api.openai.com/v1/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(completionConfig),
|
||||
})
|
||||
}),
|
||||
}
|
||||
@@ -2,8 +2,13 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
import { handler } from './handler.tsx'
|
||||
|
||||
console.log(`Function "opengraph" up and running!`)
|
||||
|
||||
Deno.serve(handler)
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, handler),
|
||||
}
|
||||
@@ -1,29 +1,30 @@
|
||||
import puppeteer from 'https://deno.land/x/puppeteer@16.2.0/mod.ts'
|
||||
import puppeteer from 'npm:puppeteer@^25'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
console.log(`wss://chrome.browserless.io?token=${Deno.env.get('PUPPETEER_BROWSERLESS_IO_KEY')}`)
|
||||
// Visit browserless.io to get your free API token
|
||||
const browser = await puppeteer.connect({
|
||||
browserWSEndpoint: `wss://chrome.browserless.io?token=${Deno.env.get(
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req) => {
|
||||
try {
|
||||
const browserWSEndpoint = `wss://chrome.browserless.io?token=${Deno.env.get(
|
||||
'PUPPETEER_BROWSERLESS_IO_KEY'
|
||||
)}`,
|
||||
})
|
||||
const page = await browser.newPage()
|
||||
)}`
|
||||
// Visit browserless.io to get your free API token
|
||||
const browser = await puppeteer.connect({
|
||||
browserWSEndpoint,
|
||||
})
|
||||
const page = await browser.newPage()
|
||||
|
||||
const url = new URL(req.url).searchParams.get('url') || 'http://www.example.com'
|
||||
const url = new URL(req.url).searchParams.get('url') || 'http://www.example.com'
|
||||
|
||||
await page.goto(url)
|
||||
const screenshot = await page.screenshot()
|
||||
await page.goto(url)
|
||||
const screenshot = await page.screenshot()
|
||||
|
||||
return new Response(screenshot, {
|
||||
headers: { 'Content-Type': 'image/png' },
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return new Response(JSON.stringify({ error: e.message }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 500,
|
||||
})
|
||||
}
|
||||
})
|
||||
return new Response(screenshot as BodyInit, {
|
||||
headers: { 'Content-Type': 'image/png' },
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return Response.json({ error: e.message }, { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -2,58 +2,26 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { createClient } from 'npm:supabase-js@2'
|
||||
// New approach (v2.95.0+)
|
||||
import { corsHeaders } from 'jsr:@supabase/supabase-js@2/cors'
|
||||
// For older versions, use hardcoded headers:
|
||||
// const corsHeaders = {
|
||||
// 'Access-Control-Allow-Origin': '*',
|
||||
// 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
// }
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// read a text file from storage and print its contents
|
||||
// This is needed if you're planning to invoke your function from a browser.
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
// read a text file from storage and print its contents
|
||||
try {
|
||||
const { data, error } = await ctx.supabase.storage.from('my-bucket').download('sample.txt')
|
||||
if (error) throw error
|
||||
|
||||
try {
|
||||
const SUPABASE_PUBLISHABLE_KEYS = JSON.parse(Deno.env.get('SUPABASE_PUBLISHABLE_KEYS')!)
|
||||
// Create a Supabase client with the Auth context of the logged in user.
|
||||
const supabaseClient = createClient(
|
||||
// Supabase API URL - env var exported by default.
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
// Supabase API publishable key - env var exported by default.
|
||||
SUPABASE_PUBLISHABLE_KEYS['default'] ?? '',
|
||||
// Create client with Auth context of the user that called the function.
|
||||
// This way your row-level-security (RLS) policies are applied.
|
||||
{
|
||||
global: {
|
||||
headers: { Authorization: req.headers.get('Authorization')! },
|
||||
},
|
||||
}
|
||||
)
|
||||
// file contents are returned as a blob, we can convert it to utf-8 text by calling text() method.
|
||||
const contents = await data.text()
|
||||
|
||||
const { data, error } = await supabaseClient.storage.from('my-bucket').download('sample.txt')
|
||||
if (error) throw error
|
||||
// prints out the contents of the file
|
||||
console.log(contents)
|
||||
|
||||
// file contents are returned as a blob, we can convert it to utf-8 text by calling text() method.
|
||||
const contents = await data.text()
|
||||
return Response.json({ contents })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
// prints out the contents of the file
|
||||
console.log(contents)
|
||||
|
||||
return new Response(JSON.stringify({ contents }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
})
|
||||
return Response.json({ error: error.message }, { status: 400 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
+16
-52
@@ -2,63 +2,27 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { createClient } from 'npm:supabase-js@2'
|
||||
// New approach (v2.95.0+)
|
||||
import { corsHeaders } from 'jsr:@supabase/supabase-js@2/cors'
|
||||
// For older versions:
|
||||
// import { corsHeaders } from '../_shared/cors.ts'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
console.log(`Function "select-from-table-with-auth-rls" up and running!`)
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
// This is needed if you're planning to invoke your function from a browser.
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
try {
|
||||
// ctx.supabase runs queries as the authenticated user, so RLS applies.
|
||||
// ctx.userClaims holds the verified user identity.
|
||||
const { data, error } = await ctx.supabase.from('users').select('*')
|
||||
if (error) throw error
|
||||
|
||||
try {
|
||||
const SUPABASE_PUBLISHABLE_KEYS = JSON.parse(Deno.env.get('SUPABASE_PUBLISHABLE_KEYS')!)
|
||||
// Create a Supabase client with the Auth context of the logged in user.
|
||||
const supabaseClient = createClient(
|
||||
// Supabase API URL - env var exported by default.
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
// Supabase API PUBLISHABLE KEY - env var exported by default.
|
||||
SUPABASE_PUBLISHABLE_KEYS['default'] ?? '',
|
||||
// Create client with Auth context of the user that called the function.
|
||||
// This way your row-level-security (RLS) policies are applied.
|
||||
{
|
||||
global: {
|
||||
headers: { Authorization: req.headers.get('Authorization')! },
|
||||
},
|
||||
}
|
||||
)
|
||||
return Response.json({ user: ctx.userClaims, data })
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 400 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
// First get the token from the Authorization header
|
||||
const token = req.headers.get('Authorization').replace('Bearer ', '')
|
||||
|
||||
// Now we can get the session or user object
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabaseClient.auth.getUser(token)
|
||||
|
||||
// And we can run queries in the context of our authenticated user
|
||||
const { data, error } = await supabaseClient.from('users').select('*')
|
||||
if (error) throw error
|
||||
|
||||
return new Response(JSON.stringify({ user, data }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// To invoke:
|
||||
// To invoke (auth: 'user' requires a signed-in user's access token):
|
||||
// curl -i --location --request POST 'http://localhost:54321/functions/v1/select-from-table-with-auth-rls' \
|
||||
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \
|
||||
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>' \
|
||||
// --header 'Content-Type: application/json' \
|
||||
// --data '{"name":"Functions"}'
|
||||
@@ -1,28 +1,26 @@
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY')
|
||||
|
||||
const handler = async (_request: Request): Promise<Response> => {
|
||||
const res = await fetch('https://api.resend.com/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${RESEND_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: 'onboarding@resend.dev',
|
||||
to: 'delivered@resend.dev',
|
||||
subject: 'hello world',
|
||||
html: '<strong>it works!</strong>',
|
||||
}),
|
||||
})
|
||||
// Accepts a signed-in user's JWT or a secret key, so deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: ['user', 'secret'] }, async (req, ctx) => {
|
||||
const res = await fetch('https://api.resend.com/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${RESEND_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: 'onboarding@resend.dev',
|
||||
to: 'delivered@resend.dev',
|
||||
subject: 'hello world',
|
||||
html: '<strong>it works!</strong>',
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
const data = await res.json()
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
return Response.json(data)
|
||||
}),
|
||||
}
|
||||
|
||||
Deno.serve(handler)
|
||||
@@ -2,7 +2,8 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import nodemailer from 'npm:nodemailer@6.9.10'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import nodemailer from 'npm:nodemailer@^9'
|
||||
|
||||
const transport = nodemailer.createTransport({
|
||||
host: Deno.env.get('SMTP_HOSTNAME')!,
|
||||
@@ -16,41 +17,39 @@ const transport = nodemailer.createTransport({
|
||||
|
||||
console.log(`Function "send-email-smtp" up and running!`)
|
||||
|
||||
Deno.serve(async (_req) => {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
transport.sendMail(
|
||||
{
|
||||
from: Deno.env.get('SMTP_FROM')!,
|
||||
to: 'testr@test.de',
|
||||
subject: `Hello from Supabase Edge Functions`,
|
||||
text: `Hello Functions \\o/`,
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
transport.sendMail(
|
||||
{
|
||||
from: Deno.env.get('SMTP_FROM')!,
|
||||
to: 'testr@test.de',
|
||||
subject: `Hello from Supabase Edge Functions`,
|
||||
text: `Hello Functions \\o/`,
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
resolve()
|
||||
}
|
||||
|
||||
resolve()
|
||||
}
|
||||
)
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(error.message, { status: 500 })
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
done: true,
|
||||
}),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
)
|
||||
})
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
done: true,
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
||||
// To invoke:
|
||||
// curl -i --location --request POST 'http://localhost:54321/functions/v1/send-email-smtp' \
|
||||
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \
|
||||
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>' \
|
||||
// --header 'Content-Type: application/json' \
|
||||
// --data '{"name":"Functions"}'
|
||||
@@ -1,16 +1,12 @@
|
||||
import * as Sentry from 'npm:@sentry/deno@^10'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
console.log('Hello from the Sentry Functions Challenge!')
|
||||
|
||||
import { createClient } from 'npm:supabase-js@2'
|
||||
import * as Sentry from 'https://deno.land/x/sentry@7.102.0/index.mjs'
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
|
||||
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default']!)
|
||||
|
||||
Sentry.init({
|
||||
dsn: Deno.env.get('SENTRY_DSN'),
|
||||
integrations: [],
|
||||
@@ -23,47 +19,47 @@ Sentry.init({
|
||||
Sentry.setTag('region', Deno.env.get('SB_REGION'))
|
||||
Sentry.setTag('execution_id', Deno.env.get('SB_EXECUTION_ID'))
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
if (req.method === 'GET') {
|
||||
return new Response(
|
||||
'What is Supabase not? POST {answer, twitter} to this URL! Need a hint? 👉 https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/sentry/index.ts'
|
||||
)
|
||||
}
|
||||
let answer: string
|
||||
let twitter: string
|
||||
// Public challenge endpoint, so deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
|
||||
try {
|
||||
const params = await req.json()
|
||||
if (!params?.answer || !params?.twitter) throw new Error('no answer')
|
||||
answer = params.answer
|
||||
twitter = params.twitter
|
||||
} catch (_) {
|
||||
return new Response('You need to send a JSON body with {answer, twitter}!')
|
||||
}
|
||||
if (answer.toLowerCase() !== 'postgres') {
|
||||
return new Response(
|
||||
`You are correct! But that means you've failed the challenge. Please try again!`
|
||||
)
|
||||
} else {
|
||||
throw { twitter }
|
||||
}
|
||||
} catch (e) {
|
||||
Sentry.captureException(e)
|
||||
const { error } = await supabase
|
||||
.from('sentry_functions_challenge')
|
||||
.insert({ twitter: e.twitter })
|
||||
if (error) console.log(e.twitter, error.message)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
msg: `Congrats, you are wrong https://itsjustpostgres.com/ 🎉 @${e.twitter} has been added to the draw!`,
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
if (req.method === 'GET') {
|
||||
return new Response(
|
||||
'What is Supabase not? POST {answer, twitter} to this URL! Need a hint? 👉 https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/sentry/index.ts'
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
let answer: string
|
||||
let twitter: string
|
||||
try {
|
||||
const params = await req.json()
|
||||
if (!params?.answer || !params?.twitter) throw new Error('no answer')
|
||||
answer = params.answer
|
||||
twitter = params.twitter
|
||||
} catch (_) {
|
||||
return new Response('You need to send a JSON body with {answer, twitter}!')
|
||||
}
|
||||
if (answer.toLowerCase() !== 'postgres') {
|
||||
return new Response(
|
||||
`You are correct! But that means you've failed the challenge. Please try again!`
|
||||
)
|
||||
} else {
|
||||
throw { twitter }
|
||||
}
|
||||
} catch (e) {
|
||||
Sentry.captureException(e)
|
||||
const { error } = await ctx.supabaseAdmin
|
||||
.from('sentry_functions_challenge')
|
||||
.insert({ twitter: e.twitter })
|
||||
if (error) console.log(e.twitter, error.message)
|
||||
return Response.json(
|
||||
{
|
||||
msg: `Congrats, you are wrong https://itsjustpostgres.com/ 🎉 @${e.twitter} has been added to the draw!`,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
/* To invoke locally:
|
||||
|
||||
@@ -71,7 +67,6 @@ Deno.serve(async (req) => {
|
||||
2. Make an HTTP request:
|
||||
|
||||
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/sentry' \
|
||||
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"answer":"mysql", "twitter":"thorwebdev"}'
|
||||
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
import * as Sentry from 'https://deno.land/x/sentry/index.mjs'
|
||||
import * as Sentry from 'npm:@sentry/deno@^10'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
dsn: Deno.env.get('SENTRY_DSN'),
|
||||
integrations: [],
|
||||
debug: true,
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0,
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: 1.0,
|
||||
})
|
||||
|
||||
// Set region and execution_id as custom tags
|
||||
Sentry.setTag('region', Deno.env.get('SB_REGION'))
|
||||
Sentry.setTag('execution_id', Deno.env.get('SB_EXECUTION_ID'))
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const { name } = await req.json()
|
||||
const data = {
|
||||
message: `Hello ${name}!`,
|
||||
}
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
try {
|
||||
const { name } = await req.json()
|
||||
const data = {
|
||||
message: `Hello ${name}!`,
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } })
|
||||
} catch (e) {
|
||||
Sentry.captureException(e)
|
||||
return new Response(JSON.stringify({ msg: 'error' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
})
|
||||
return Response.json(data)
|
||||
} catch (e) {
|
||||
Sentry.captureException(e)
|
||||
return Response.json({ msg: 'error' }, { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -1,32 +1,35 @@
|
||||
import { WebClient } from 'https://deno.land/x/slack_web_api@6.7.2/mod.js'
|
||||
import { WebClient } from 'npm:@slack/web-api@^7'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const slackBotToken = Deno.env.get('SLACK_TOKEN') ?? ''
|
||||
const botClient = new WebClient(slackBotToken)
|
||||
|
||||
console.log(`Slack URL verification function up and running!`)
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const { token, challenge, type, event } = await req.json()
|
||||
|
||||
if (type == 'url_verification') {
|
||||
return new Response(JSON.stringify({ challenge }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
})
|
||||
} else if (event.type == 'app_mention') {
|
||||
const { user, text, channel, ts } = event
|
||||
// Here you should process the text received and return a response:
|
||||
const response = await botClient.chat.postMessage({
|
||||
channel: channel,
|
||||
text: `Hello <@${user}>!`,
|
||||
thread_ts: ts,
|
||||
})
|
||||
return new Response('ok', { status: 200 })
|
||||
// Public endpoint, so deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
|
||||
try {
|
||||
// Implement your Slack request signature verification here before trusting the payload
|
||||
// (validate `x-slack-signature` / `x-slack-request-timestamp` with your signing secret).
|
||||
const { challenge, type, event } = await req.json()
|
||||
|
||||
if (type == 'url_verification') {
|
||||
return Response.json({ challenge })
|
||||
} else if (event.type == 'app_mention') {
|
||||
const { user, text, channel, ts } = event
|
||||
// Here you should process the text received and return a response:
|
||||
await botClient.chat.postMessage({
|
||||
channel: channel,
|
||||
text: `Hello <@${user}>!`,
|
||||
thread_ts: ts,
|
||||
})
|
||||
return new Response('ok', { status: 200 })
|
||||
}
|
||||
|
||||
return Response.json({ ok: true })
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 500,
|
||||
})
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
@@ -1,24 +1,29 @@
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const msg = new TextEncoder().encode('data: hello\r\n\r\n')
|
||||
|
||||
Deno.serve((_) => {
|
||||
let timerId: number | undefined
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, (req, ctx) => {
|
||||
let timerId: number | undefined
|
||||
|
||||
const body = new ReadableStream({
|
||||
start(controller) {
|
||||
timerId = setInterval(() => {
|
||||
controller.enqueue(msg)
|
||||
}, 1000)
|
||||
},
|
||||
cancel() {
|
||||
if (typeof timerId === 'number') {
|
||||
clearInterval(timerId)
|
||||
}
|
||||
},
|
||||
})
|
||||
const body = new ReadableStream({
|
||||
start(controller) {
|
||||
timerId = setInterval(() => {
|
||||
controller.enqueue(msg)
|
||||
}, 1000)
|
||||
},
|
||||
cancel() {
|
||||
if (typeof timerId === 'number') {
|
||||
clearInterval(timerId)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(body, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
},
|
||||
})
|
||||
})
|
||||
return new Response(body, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
},
|
||||
})
|
||||
}),
|
||||
}
|
||||
@@ -2,37 +2,36 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
// Import via bare specifier thanks to the import_map.json file.
|
||||
import Stripe from 'https://esm.sh/stripe@14?target=denonext'
|
||||
import Stripe from 'npm:stripe@^22'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const stripe = new Stripe(Deno.env.get('STRIPE_API_KEY') as string, {
|
||||
// This is needed to use the Fetch API rather than relying on the Node http
|
||||
// package.
|
||||
apiVersion: '2024-11-20',
|
||||
})
|
||||
const stripe = new Stripe(Deno.env.get('STRIPE_API_KEY') as string)
|
||||
// This is needed in order to use the Web Crypto API in Deno.
|
||||
const cryptoProvider = Stripe.createSubtleCryptoProvider()
|
||||
|
||||
console.log('Hello from Stripe Webhook!')
|
||||
|
||||
Deno.serve(async (request) => {
|
||||
const signature = request.headers.get('Stripe-Signature')
|
||||
// Stripe verifies the request via its signature, so deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req) => {
|
||||
const signature = req.headers.get('Stripe-Signature')
|
||||
|
||||
// First step is to verify the event. The .text() method must be used as the
|
||||
// verification relies on the raw request body rather than the parsed JSON.
|
||||
const body = await request.text()
|
||||
let receivedEvent
|
||||
try {
|
||||
receivedEvent = await stripe.webhooks.constructEventAsync(
|
||||
body,
|
||||
signature!,
|
||||
Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!,
|
||||
undefined,
|
||||
cryptoProvider
|
||||
)
|
||||
} catch (err) {
|
||||
return new Response(err.message, { status: 400 })
|
||||
}
|
||||
console.log(`🔔 Event received: ${receivedEvent.id}`)
|
||||
return new Response(JSON.stringify({ ok: true }), { status: 200 })
|
||||
})
|
||||
// First step is to verify the event. The .text() method must be used as the
|
||||
// verification relies on the raw request body rather than the parsed JSON.
|
||||
const body = await req.text()
|
||||
let receivedEvent
|
||||
try {
|
||||
receivedEvent = await stripe.webhooks.constructEventAsync(
|
||||
body,
|
||||
signature!,
|
||||
Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!,
|
||||
undefined,
|
||||
cryptoProvider
|
||||
)
|
||||
} catch (err) {
|
||||
return new Response(err.message, { status: 400 })
|
||||
}
|
||||
console.log(`🔔 Event received: ${receivedEvent.id}`)
|
||||
return Response.json({ ok: true })
|
||||
}),
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Bot, webhookCallback } from 'npm:grammy@^1'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
console.log(`Function "telegram-bot" up and running!`)
|
||||
|
||||
import { Bot, webhookCallback } from 'https://deno.land/x/grammy@v1.8.3/mod.ts'
|
||||
|
||||
const bot = new Bot(Deno.env.get('TELEGRAM_BOT_TOKEN') || '')
|
||||
|
||||
bot.command('start', (ctx) => ctx.reply('Welcome! Up and running.'))
|
||||
@@ -14,15 +15,19 @@ bot.command('ping', (ctx) => ctx.reply(`Pong! ${new Date()} ${Date.now()}`))
|
||||
|
||||
const handleUpdate = webhookCallback(bot, 'std/http')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const url = new URL(req.url)
|
||||
if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) {
|
||||
return new Response('not allowed', { status: 405 })
|
||||
}
|
||||
// Telegram is verified via the secret query param, so deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req) => {
|
||||
try {
|
||||
const url = new URL(req.url)
|
||||
if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) {
|
||||
return new Response('not allowed', { status: 405 })
|
||||
}
|
||||
|
||||
return await handleUpdate(req)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
return await handleUpdate(req)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return new Response('Internal Server Error', { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import React from 'https://esm.sh/react@18.2.0?deno-std=0.140.0'
|
||||
import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts'
|
||||
import { createClient } from 'jsr:@supabase/supabase-js@2'
|
||||
import { corsHeaders } from '../_shared/cors.ts'
|
||||
import { getTweets } from './getTweet.ts'
|
||||
import Tweet from './Tweet.tsx'
|
||||
|
||||
const STORAGE_URL =
|
||||
'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/tweet-to-image'
|
||||
|
||||
// Load custom font
|
||||
const FONT_URL = `${STORAGE_URL}/CircularStd-Book.otf`
|
||||
const font = fetch(new URL(FONT_URL, import.meta.url)).then((res) => res.arrayBuffer())
|
||||
|
||||
export async function handler(req: Request) {
|
||||
const url = new URL(req.url)
|
||||
const tweetId = url.searchParams.get('tweetId')
|
||||
|
||||
if (!tweetId)
|
||||
return new Response(JSON.stringify({ error: 'missing tweetId param' }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
|
||||
try {
|
||||
// Try to get image from Supabase Storage CDN.
|
||||
const storageResponse = await fetch(`${STORAGE_URL}/${tweetId}.png`)
|
||||
if (storageResponse.ok) return storageResponse
|
||||
|
||||
// Else, generate image and upload to storage.
|
||||
const fontData = await font
|
||||
|
||||
const tweets = await getTweets([tweetId])
|
||||
const tweet = tweets[0]
|
||||
console.log('formattedTweets', JSON.stringify(tweets, null, 2))
|
||||
const generatedImage = new ImageResponse(
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#1c1c1c',
|
||||
color: '#EDEDED',
|
||||
}}
|
||||
>
|
||||
<Tweet {...tweet} />
|
||||
</div>,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
// Supported options: 'twemoji', 'blobmoji', 'noto', 'openmoji', 'fluent', 'fluentFlat'
|
||||
// Default to 'twemoji'
|
||||
emoji: 'twemoji',
|
||||
fonts: [
|
||||
{
|
||||
name: 'Circular',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
|
||||
const supabaseAdminClient = createClient(
|
||||
// Supabase API URL - env var exported by default when deployed.
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
// Supabase API SECRET KEY - env var exported by default when deployed.
|
||||
SUPABASE_SECRET_KEYS['default'] ?? ''
|
||||
)
|
||||
|
||||
// Upload image to storage.
|
||||
const { error } = await supabaseAdminClient.storage
|
||||
.from('images')
|
||||
.upload(`tweet-to-image/${tweetId}.png`, generatedImage.body!, {
|
||||
contentType: 'image/png',
|
||||
cacheControl: '31536000',
|
||||
upsert: false,
|
||||
})
|
||||
if (error) throw error
|
||||
|
||||
return generatedImage
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,89 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { handler } from './handler.tsx'
|
||||
import { ImageResponse } from 'npm:@vercel/og@^0'
|
||||
import React from 'npm:react@19'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
import { getTweets } from './getTweet.ts'
|
||||
import Tweet from './Tweet.tsx'
|
||||
|
||||
const STORAGE_URL =
|
||||
'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/tweet-to-image'
|
||||
|
||||
// Load custom font
|
||||
const FONT_URL = `${STORAGE_URL}/CircularStd-Book.otf`
|
||||
const font = fetch(new URL(FONT_URL, import.meta.url)).then((res) => res.arrayBuffer())
|
||||
|
||||
console.log(`Function "tweet-to-image" up and running!`)
|
||||
|
||||
Deno.serve(handler)
|
||||
async function handler(req: Request, ctx) {
|
||||
const url = new URL(req.url)
|
||||
const tweetId = url.searchParams.get('tweetId')
|
||||
|
||||
if (!tweetId) return Response.json({ error: 'missing tweetId param' }, { status: 400 })
|
||||
|
||||
try {
|
||||
// Try to get image from Supabase Storage CDN.
|
||||
const storageResponse = await fetch(`${STORAGE_URL}/${tweetId}.png`)
|
||||
if (storageResponse.ok) return storageResponse
|
||||
|
||||
// Else, generate image and upload to storage.
|
||||
const fontData = await font
|
||||
|
||||
const tweets = await getTweets([tweetId])
|
||||
const tweet = tweets[0]
|
||||
console.log('formattedTweets', JSON.stringify(tweets, null, 2))
|
||||
const generatedImage = new ImageResponse(
|
||||
React.createElement(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#1c1c1c',
|
||||
color: '#EDEDED',
|
||||
},
|
||||
},
|
||||
React.createElement(Tweet, { ...tweet })
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
// Supported options: 'twemoji', 'blobmoji', 'noto', 'openmoji', 'fluent', 'fluentFlat'
|
||||
// Default to 'twemoji'
|
||||
emoji: 'twemoji',
|
||||
fonts: [
|
||||
{
|
||||
name: 'Circular',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
// Upload image to storage.
|
||||
const { error } = await ctx.supabaseAdmin.storage
|
||||
.from('images')
|
||||
.upload(`tweet-to-image/${tweetId}.png`, generatedImage.body!, {
|
||||
contentType: 'image/png',
|
||||
cacheControl: '31536000',
|
||||
upsert: false,
|
||||
})
|
||||
if (error) throw error
|
||||
|
||||
return generatedImage
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
// Uploads to Storage with a secret key, so deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'secret' }, handler),
|
||||
}
|
||||
@@ -2,47 +2,51 @@
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { Redis } from 'https://deno.land/x/upstash_redis@v1.19.3/mod.ts'
|
||||
import { Redis } from 'npm:@upstash/redis@^1'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
console.log(`Function "upstash-redis-counter" up and running!`)
|
||||
|
||||
Deno.serve(async (_req) => {
|
||||
try {
|
||||
const redis = new Redis({
|
||||
url: Deno.env.get('UPSTASH_REDIS_REST_URL')!,
|
||||
token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!,
|
||||
})
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (_req) => {
|
||||
try {
|
||||
const redis = new Redis({
|
||||
url: Deno.env.get('UPSTASH_REDIS_REST_URL')!,
|
||||
token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!,
|
||||
})
|
||||
|
||||
const deno_region = Deno.env.get('DENO_REGION')
|
||||
if (deno_region) {
|
||||
// Increment region counter
|
||||
await redis.hincrby('supa-edge-counter', deno_region, 1)
|
||||
} else {
|
||||
// Increment localhost counter
|
||||
await redis.hincrby('supa-edge-counter', 'localhost', 1)
|
||||
const deno_region = Deno.env.get('DENO_REGION')
|
||||
if (deno_region) {
|
||||
// Increment region counter
|
||||
await redis.hincrby('supa-edge-counter', deno_region, 1)
|
||||
} else {
|
||||
// Increment localhost counter
|
||||
await redis.hincrby('supa-edge-counter', 'localhost', 1)
|
||||
}
|
||||
|
||||
// Get all values
|
||||
const counterHash: Record<string, number> | null = await redis.hgetall('supa-edge-counter')
|
||||
const counters = Object.entries(counterHash!)
|
||||
.sort(([, a], [, b]) => b - a) // sort desc
|
||||
.reduce(
|
||||
(r, [k, v]) => ({
|
||||
total: r.total + v,
|
||||
regions: { ...r.regions, [k]: v },
|
||||
}),
|
||||
{
|
||||
total: 0,
|
||||
regions: {},
|
||||
}
|
||||
)
|
||||
|
||||
return Response.json({ counters })
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
// Get all values
|
||||
const counterHash: Record<string, number> | null = await redis.hgetall('supa-edge-counter')
|
||||
const counters = Object.entries(counterHash!)
|
||||
.sort(([, a], [, b]) => b - a) // sort desc
|
||||
.reduce(
|
||||
(r, [k, v]) => ({
|
||||
total: r.total + v,
|
||||
regions: { ...r.regions, [k]: v },
|
||||
}),
|
||||
{
|
||||
total: 0,
|
||||
regions: {},
|
||||
}
|
||||
)
|
||||
|
||||
return new Response(JSON.stringify({ counters }), { status: 200 })
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 200,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// To invoke, navigate to 'http://localhost:54321/functions/v1/upstash-redis-counter'.
|
||||
// To invoke:
|
||||
// curl -i --location --request GET 'http://localhost:54321/functions/v1/upstash-redis-counter' \
|
||||
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>'
|
||||
@@ -1,67 +1,41 @@
|
||||
import { Redis } from 'https://deno.land/x/upstash_redis@v1.19.3/mod.ts'
|
||||
import { Ratelimit } from 'https://cdn.skypack.dev/@upstash/ratelimit@0.4.4'
|
||||
import { createClient } from 'npm:supabase-js@2'
|
||||
import { Ratelimit } from 'npm:@upstash/ratelimit@^2'
|
||||
import { Redis } from 'npm:@upstash/redis@^1'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
console.log(`Function "upstash-redis-counter" up and running!`)
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const SUPABASE_PUBLISHABLE_KEYS = JSON.parse(Deno.env.get('SUPABASE_PUBLISHABLE_KEYS')!)
|
||||
// Create a Supabase client with the Auth context of the logged in user.
|
||||
const supabaseClient = createClient(
|
||||
// Supabase API URL - env var exported by default.
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
// Supabase publishable key - env var exported by default.
|
||||
SUPABASE_PUBLISHABLE_KEYS['default'] ?? '',
|
||||
// Create client with Auth context of the user that called the function.
|
||||
// This way your row-level-security (RLS) policies are applied.
|
||||
{
|
||||
global: {
|
||||
headers: { Authorization: req.headers.get('Authorization')! },
|
||||
},
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
try {
|
||||
const redis = new Redis({
|
||||
url: Deno.env.get('UPSTASH_REDIS_REST_URL')!,
|
||||
token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!,
|
||||
})
|
||||
|
||||
// Create a new ratelimiter, that allows 10 requests per 10 seconds
|
||||
const ratelimit = new Ratelimit({
|
||||
redis,
|
||||
limiter: Ratelimit.slidingWindow(2, '10 s'),
|
||||
analytics: true,
|
||||
})
|
||||
|
||||
// Use a constant string to limit all requests with a single ratelimit
|
||||
// Or use a userID, apiKey or ip address for individual limits.
|
||||
const identifier = ctx.userClaims?.id
|
||||
const { success } = await ratelimit.limit(identifier)
|
||||
|
||||
if (!success) {
|
||||
throw new Error('limit exceeded')
|
||||
}
|
||||
)
|
||||
|
||||
// First get the token from the Authorization header
|
||||
const token = req.headers.get('Authorization').replace('Bearer ', '')
|
||||
|
||||
// Now we can get the session or user object
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabaseClient.auth.getUser(token)
|
||||
if (!user) throw new Error('no user')
|
||||
console.log(user.id)
|
||||
|
||||
const redis = new Redis({
|
||||
url: Deno.env.get('UPSTASH_REDIS_REST_URL')!,
|
||||
token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!,
|
||||
})
|
||||
|
||||
// Create a new ratelimiter, that allows 10 requests per 10 seconds
|
||||
const ratelimit = new Ratelimit({
|
||||
redis,
|
||||
limiter: Ratelimit.slidingWindow(2, '10 s'),
|
||||
analytics: true,
|
||||
})
|
||||
|
||||
// Use a constant string to limit all requests with a single ratelimit
|
||||
// Or use a userID, apiKey or ip address for individual limits.
|
||||
const identifier = user.id
|
||||
const { success } = await ratelimit.limit(identifier)
|
||||
|
||||
if (!success) {
|
||||
throw new Error('limit exceeded')
|
||||
return Response.json({ success })
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message })
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ success }), { status: 200 })
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 200,
|
||||
})
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
||||
// curl -i --location --request POST 'http://localhost:54321/functions/v1/upstash-redis-ratelimit' \
|
||||
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc4MDA4Mjk2LCJzdWIiOiJkZjQ3ZDU5My0zMjY1LTQ2OWEtOWI5OS1mZDk1OTdhOGU4YzciLCJlbWFpbCI6InRlc3RyQHRlc3Quc2ciLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY3ODAwNDY5Nn1dLCJzZXNzaW9uX2lkIjoiMDNjMmFlYjUtMjk5MC00ZWU0LWIxYTYtZmU1Y2IwMGFhMjU3In0.WwqggrX34j0NINiulC9rsj-cd6HzV55ySxJkJlVsU4o' \
|
||||
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>' \
|
||||
// --header 'Content-Type: application/json' \
|
||||
// --data '{"name":"Functions"}'
|
||||
@@ -1,8 +1,11 @@
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
import { add } from './add-wasm/pkg/add_wasm.js'
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const { a, b } = await req.json()
|
||||
return new Response(JSON.stringify({ result: add(a, b) }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
})
|
||||
// Authenticated endpoint, so deploy with verify_jwt = true.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req) => {
|
||||
const { a, b } = await req.json()
|
||||
return Response.json({ result: add(a, b) })
|
||||
}),
|
||||
}
|
||||
@@ -1,32 +1,24 @@
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const ALLOWED_ORIGINS = ['http://localhost:8000']
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': ALLOWED_ORIGINS.join(','),
|
||||
'Access-Control-Allow-Headers':
|
||||
'authorization, x-client-info, apikey, content-type, range, if-match',
|
||||
'Access-Control-Expose-Headers': 'range, accept-ranges, etag',
|
||||
'Access-Control-Max-Age': '300',
|
||||
|
||||
// Public tile proxy, so deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, (req) => {
|
||||
// Restrict which origins may read the private tiles.
|
||||
const origin = req.headers.get('Origin')
|
||||
if (!origin || !ALLOWED_ORIGINS.includes(origin)) {
|
||||
return new Response('Not Allowed', { status: 405 })
|
||||
}
|
||||
|
||||
const reqUrl = new URL(req.url)
|
||||
const url = `${Deno.env.get('SUPABASE_URL')}/storage/v1/object/authenticated${reqUrl.pathname}`
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
const { method, headers } = req
|
||||
// Add Auth header so Storage serves the private object.
|
||||
const modHeaders = new Headers(headers)
|
||||
modHeaders.append('authorization', `Bearer ${SUPABASE_SECRET_KEYS['default']!}`)
|
||||
return fetch(url, { method, headers: modHeaders })
|
||||
}),
|
||||
}
|
||||
|
||||
Deno.serve((req) => {
|
||||
// This is needed if you're planning to invoke your function from a browser.
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
// Check origin
|
||||
const origin = req.headers.get('Origin')
|
||||
|
||||
if (!origin || !ALLOWED_ORIGINS.includes(origin)) {
|
||||
return new Response('Not Allowed', { status: 405 })
|
||||
}
|
||||
|
||||
const reqUrl = new URL(req.url)
|
||||
const url = `${Deno.env.get('SUPABASE_URL')}/storage/v1/object/authenticated${reqUrl.pathname}`
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
const { method, headers } = req
|
||||
// Add Auth header
|
||||
const modHeaders = new Headers(headers)
|
||||
modHeaders.append('authorization', `Bearer ${SUPABASE_SECRET_KEYS['default']!}`)
|
||||
return fetch(url, { method, headers: modHeaders })
|
||||
})
|
||||
@@ -22,7 +22,7 @@ public = true
|
||||
|
||||
[functions.create-upload-token]
|
||||
enabled = true
|
||||
verify_jwt = true
|
||||
verify_jwt = false
|
||||
import_map = "./functions/create-upload-token/deno.json"
|
||||
# Uncomment to specify a custom file path to the entrypoint.
|
||||
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||
|
||||
+21
-25
@@ -1,31 +1,27 @@
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
import { createClient } from 'jsr:@supabase/supabase-js'
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const SUPABASE_URL = Deno.env.get('SUPABASE_URL') ?? ''
|
||||
const SUPABASE_SECRET_KEY = SUPABASE_SECRET_KEYS['default'] ?? ''
|
||||
// Deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => {
|
||||
try {
|
||||
const { filename } = await req.json()
|
||||
if (!filename) {
|
||||
return Response.json({ error: 'Missing filename' }, { status: 400 })
|
||||
}
|
||||
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_SECRET_KEY)
|
||||
const { data, error } = await ctx.supabaseAdmin.storage
|
||||
.from('uploads')
|
||||
.createSignedUploadUrl(filename)
|
||||
|
||||
try {
|
||||
const { filename } = await req.json()
|
||||
if (!filename) {
|
||||
return new Response('Missing filename', { status: 400 })
|
||||
if (error) {
|
||||
return Response.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
|
||||
return Response.json({ token: data.token })
|
||||
} catch (error) {
|
||||
return Response.json({ error: (error as Error).message }, { status: 500 })
|
||||
}
|
||||
|
||||
const { data, error } = await supabase.storage.from('uploads').createSignedUploadUrl(filename)
|
||||
|
||||
if (error) {
|
||||
return new Response(error.message, { status: 500 })
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ token: data.token }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response((error as Error).message, { status: 500 })
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
@@ -15,6 +15,10 @@ extra_search_path = ["public", "extensions"]
|
||||
# for accidental or malicious requests.
|
||||
max_rows = 1000
|
||||
|
||||
# Triggered by a Database Webhook with a secret key, so skip the platform JWT check.
|
||||
[functions.push]
|
||||
verify_jwt = false
|
||||
|
||||
[db]
|
||||
# Port to use for the local database URL.
|
||||
port = 54322
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
import { createClient } from 'jsr:@supabase/supabase-js@2'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
console.log('Hello from Functions!')
|
||||
|
||||
@@ -19,37 +19,39 @@ interface WebhookPayload {
|
||||
old_record: null | Notification
|
||||
}
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default']!)
|
||||
// Deploy with verify_jwt = false.
|
||||
export default {
|
||||
fetch: withSupabase<any>({ auth: 'secret' }, async (req, ctx) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
const { data } = await ctx.supabaseAdmin
|
||||
.from('profiles')
|
||||
.select('expo_push_token')
|
||||
.eq('id', payload.record.user_id)
|
||||
.single()
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
const { data } = await supabase
|
||||
.from('profiles')
|
||||
.select('expo_push_token')
|
||||
.eq('id', payload.record.user_id)
|
||||
.single()
|
||||
if (!data?.expo_push_token) {
|
||||
return Response.json({ error: 'Expo push token not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const res = await fetch('https://exp.host/--/api/v2/push/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${Deno.env.get('EXPO_ACCESS_TOKEN')}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
to: data?.expo_push_token,
|
||||
sound: 'default',
|
||||
body: payload.record.body,
|
||||
}),
|
||||
}).then((res) => res.json())
|
||||
const res = await fetch('https://exp.host/--/api/v2/push/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${Deno.env.get('EXPO_ACCESS_TOKEN')}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
to: data.expo_push_token,
|
||||
sound: 'default',
|
||||
body: payload.record.body,
|
||||
}),
|
||||
}).then((res) => res.json())
|
||||
|
||||
return new Response(JSON.stringify(res), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
})
|
||||
return Response.json(res)
|
||||
}),
|
||||
}
|
||||
|
||||
// To invoke:
|
||||
// curl -i --location --request POST 'http://localhost:54321/functions/v1/' \
|
||||
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
|
||||
// curl -i --location --request POST 'http://localhost:54321/functions/v1/push' \
|
||||
// --header 'apikey: <SUPABASE_SECRET_KEY>' \
|
||||
// --header 'Content-Type: application/json' \
|
||||
// --data '{"name":"Functions"}'
|
||||
Reference in new issue
Block a user