diff --git a/examples/ai/aws_bedrock_image_gen/supabase/config.toml b/examples/ai/aws_bedrock_image_gen/supabase/config.toml index de3c5ea0f71..5d8dd5e76b5 100644 --- a/examples/ai/aws_bedrock_image_gen/supabase/config.toml +++ b/examples/ai/aws_bedrock_image_gen/supabase/config.toml @@ -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 diff --git a/examples/ai/aws_bedrock_image_gen/supabase/functions/image_gen/index.ts b/examples/ai/aws_bedrock_image_gen/supabase/functions/image_gen/index.ts index e20b971cfd5..b3b596daa1e 100644 --- a/examples/ai/aws_bedrock_image_gen/supabase/functions/image_gen/index.ts +++ b/examples/ai/aws_bedrock_image_gen/supabase/functions/image_gen/index.ts @@ -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: ' \ --header 'Content-Type: application/json' \ --data '{"prompt":"A beautiful picture of a bird"}' */ diff --git a/examples/ai/edge-functions/README.md b/examples/ai/edge-functions/README.md index 64800461b9b..7d8665bd0e7 100644 --- a/examples/ai/edge-functions/README.md +++ b/examples/ai/edge-functions/README.md @@ -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://.supabase.co/functions/v1/search' \ - --header 'apikey: ' \ + --header 'apikey: ' \ --header 'Content-Type: application/json' \ --data '{"search":"vehicles"}' ``` diff --git a/examples/ai/edge-functions/supabase/config.toml b/examples/ai/edge-functions/supabase/config.toml index 2cea162eae1..9437d261137 100644 --- a/examples/ai/edge-functions/supabase/config.toml +++ b/examples/ai/edge-functions/supabase/config.toml @@ -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 diff --git a/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts b/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts index 034bed58306..48c0c9fcae6 100644 --- a/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts +++ b/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts @@ -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( - 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({ 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' }) + }), +} diff --git a/examples/ai/edge-functions/supabase/functions/search/index.ts b/examples/ai/edge-functions/supabase/functions/search/index.ts index 2497e88b4fa..b519d0bb0b6 100644 --- a/examples/ai/edge-functions/supabase/functions/search/index.ts +++ b/examples/ai/edge-functions/supabase/functions/search/index.ts @@ -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( - 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({ 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: ' \ --header 'Content-Type: application/json' \ --data '{"search":"vehicles"}' diff --git a/examples/ai/edge-functions/supabase/migrations/20240410041607_database-webhook.sql b/examples/ai/edge-functions/supabase/migrations/20240410041607_database-webhook.sql index 40bbf7ca73c..7b5689e8c89 100644 --- a/examples/ai/edge-functions/supabase/migrations/20240410041607_database-webhook.sql +++ b/examples/ai/edge-functions/supabase/migrations/20240410041607_database-webhook.sql @@ -1,6 +1,6 @@ --- Insert your and below, then uncomment the trigger creation and run `supabase db push` +-- Insert your and 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://.supabase.co/functions/v1/generate-embedding', -- 'POST', --- '{"Content-type":"application/json","apikey":""}', +-- '{"Content-type":"application/json","apikey":""}', -- '{}', -- '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":""}', -- '{}', -- '5000' -- ); diff --git a/examples/ai/llamafile-edge/supabase/config.toml b/examples/ai/llamafile-edge/supabase/config.toml index 5e68435ec57..f585053b378 100644 --- a/examples/ai/llamafile-edge/supabase/config.toml +++ b/examples/ai/llamafile-edge/supabase/config.toml @@ -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 diff --git a/examples/ai/llamafile-edge/supabase/functions/llamafile-stream/index.ts b/examples/ai/llamafile-edge/supabase/functions/llamafile-stream/index.ts index 538691f0aa6..e320130a4d6 100644 --- a/examples/ai/llamafile-edge/supabase/functions/llamafile-stream/index.ts +++ b/examples/ai/llamafile-edge/supabase/functions/llamafile-stream/index.ts @@ -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 - - 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 - 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: " */ diff --git a/examples/ai/llamafile-edge/supabase/functions/llamafile/index.ts b/examples/ai/llamafile-edge/supabase/functions/llamafile/index.ts index fa6d03e416e..2582f54dce7 100644 --- a/examples/ai/llamafile-edge/supabase/functions/llamafile/index.ts +++ b/examples/ai/llamafile-edge/supabase/functions/llamafile/index.ts @@ -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: " */ diff --git a/examples/ai/llamafile-edge/supabase/functions/openai-sdk/index.ts b/examples/ai/llamafile-edge/supabase/functions/openai-sdk/index.ts index 1dee961ec67..84fb09ea6b8 100644 --- a/examples/ai/llamafile-edge/supabase/functions/openai-sdk/index.ts +++ b/examples/ai/llamafile-edge/supabase/functions/openai-sdk/index.ts @@ -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: ' \ --header 'Content-Type: application/json' \ --data '{"prompt":"Who are you?"}' diff --git a/examples/edge-functions/supabase/config.toml b/examples/edge-functions/supabase/config.toml index 7b00c661364..6ea94af249a 100644 --- a/examples/edge-functions/supabase/config.toml +++ b/examples/edge-functions/supabase/config.toml @@ -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"] diff --git a/examples/edge-functions/supabase/functions/auth-hook-react-email-resend/_templates/magic-link.tsx b/examples/edge-functions/supabase/functions/auth-hook-react-email-resend/_templates/magic-link.tsx index 6f7cb81760b..cb7cfee30b4 100644 --- a/examples/edge-functions/supabase/functions/auth-hook-react-email-resend/_templates/magic-link.tsx +++ b/examples/edge-functions/supabase/functions/auth-hook-react-email-resend/_templates/magic-link.tsx @@ -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 diff --git a/examples/edge-functions/supabase/functions/auth-hook-react-email-resend/_templates/sign-up.tsx b/examples/edge-functions/supabase/functions/auth-hook-react-email-resend/_templates/sign-up.tsx index 6a02f9f1582..0fb7c76215b 100644 --- a/examples/edge-functions/supabase/functions/auth-hook-react-email-resend/_templates/sign-up.tsx +++ b/examples/edge-functions/supabase/functions/auth-hook-react-email-resend/_templates/sign-up.tsx @@ -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 diff --git a/examples/edge-functions/supabase/functions/auth-hook-react-email-resend/index.ts b/examples/edge-functions/supabase/functions/auth-hook-react-email-resend/index.ts index ecf988aff08..97eee493daf 100644 --- a/examples/edge-functions/supabase/functions/auth-hook-react-email-resend/index.ts +++ b/examples/edge-functions/supabase/functions/auth-hook-react-email-resend/index.ts @@ -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 ', + 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 ', - 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 }) + }), +} diff --git a/examples/edge-functions/supabase/functions/background-upload-storage/index.ts b/examples/edge-functions/supabase/functions/background-upload-storage/index.ts index 49fdb1ea55c..c1c4a36f22d 100644 --- a/examples/edge-functions/supabase/functions/background-upload-storage/index.ts +++ b/examples/edge-functions/supabase/functions/background-upload-storage/index.ts @@ -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 +type StorageFileApi = ReturnType type StorageUploadPromise = ReturnType 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 }) + } + }), +} diff --git a/examples/edge-functions/supabase/functions/browser-with-cors/index.ts b/examples/edge-functions/supabase/functions/browser-with-cors/index.ts index ae656f8859d..990ed5c51c7 100644 --- a/examples/edge-functions/supabase/functions/browser-with-cors/index.ts +++ b/examples/edge-functions/supabase/functions/browser-with-cors/index.ts @@ -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 ' \ // --header 'Content-Type: application/json' \ // --data '{"name":"Functions"}' diff --git a/examples/edge-functions/supabase/functions/cloudflare-turnstile/index.ts b/examples/edge-functions/supabase/functions/cloudflare-turnstile/index.ts index 5020642bbde..ea03041141a 100644 --- a/examples/edge-functions/supabase/functions/cloudflare-turnstile/index.ts +++ b/examples/edge-functions/supabase/functions/cloudflare-turnstile/index.ts @@ -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"}' diff --git a/examples/edge-functions/supabase/functions/custom-jwt-validation/index.ts b/examples/edge-functions/supabase/functions/custom-jwt-validation/index.ts index bfedbaaede9..32346d7d402 100644 --- a/examples/edge-functions/supabase/functions/custom-jwt-validation/index.ts +++ b/examples/edge-functions/supabase/functions/custom-jwt-validation/index.ts @@ -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) + }) + ), +} diff --git a/examples/edge-functions/supabase/functions/elevenlabs-speech-to-text/index.ts b/examples/edge-functions/supabase/functions/elevenlabs-speech-to-text/index.ts index 56905387d63..c325a922a0f 100644 --- a/examples/edge-functions/supabase/functions/elevenlabs-speech-to-text/index.ts +++ b/examples/edge-functions/supabase/functions/elevenlabs-speech-to-text/index.ts @@ -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): 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) - } -}) + }), +} diff --git a/examples/edge-functions/supabase/functions/elevenlabs-text-to-speech/index.ts b/examples/edge-functions/supabase/functions/elevenlabs-text-to-speech/index.ts index b142f0b0f10..6b916c06998 100644 --- a/examples/edge-functions/supabase/functions/elevenlabs-text-to-speech/index.ts +++ b/examples/edge-functions/supabase/functions/elevenlabs-text-to-speech/index.ts @@ -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): 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 }) + } + }), +} diff --git a/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts b/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts index 20e8f0d4f5d..2352932a941 100644 --- a/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts +++ b/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts @@ -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({ 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' diff --git a/examples/edge-functions/supabase/functions/github-action-deploy/index.ts b/examples/edge-functions/supabase/functions/github-action-deploy/index.ts index e6c7d57d572..8b4dee968ab 100644 --- a/examples/edge-functions/supabase/functions/github-action-deploy/index.ts +++ b/examples/edge-functions/supabase/functions/github-action-deploy/index.ts @@ -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 ' diff --git a/examples/edge-functions/supabase/functions/huggingface-image-captioning/index.ts b/examples/edge-functions/supabase/functions/huggingface-image-captioning/index.ts index 790ee619019..e599acbc59e 100644 --- a/examples/edge-functions/supabase/functions/huggingface-image-captioning/index.ts +++ b/examples/edge-functions/supabase/functions/huggingface-image-captioning/index.ts @@ -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( - // 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({ 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' }) + }), +} diff --git a/examples/edge-functions/supabase/functions/image-manipulation/index.ts b/examples/edge-functions/supabase/functions/image-manipulation/index.ts index a224b58a214..760f84d78f3 100644 --- a/examples/edge-functions/supabase/functions/image-manipulation/index.ts +++ b/examples/edge-functions/supabase/functions/image-manipulation/index.ts @@ -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' }, + }) + }), +} diff --git a/examples/edge-functions/supabase/functions/kysely-postgres/index.ts b/examples/edge-functions/supabase/functions/kysely-postgres/index.ts index c0d257dcee9..6b5b3ab7b4a 100644 --- a/examples/edge-functions/supabase/functions/kysely-postgres/index.ts +++ b/examples/edge-functions/supabase/functions/kysely-postgres/index.ts @@ -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({ }, }) -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 ' diff --git a/examples/edge-functions/supabase/functions/location/index.ts b/examples/edge-functions/supabase/functions/location/index.ts index 530214a0bde..25770ef03e9 100644 --- a/examples/edge-functions/supabase/functions/location/index.ts +++ b/examples/edge-functions/supabase/functions/location/index.ts @@ -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, + }) + } + }), +} diff --git a/examples/edge-functions/supabase/functions/mcp/simple-mcp-server/index.ts b/examples/edge-functions/supabase/functions/mcp/simple-mcp-server/index.ts index af803fa6028..bf7ecad3c17 100644 --- a/examples/edge-functions/supabase/functions/mcp/simple-mcp-server/index.ts +++ b/examples/edge-functions/supabase/functions/mcp/simple-mcp-server/index.ts @@ -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), +} diff --git a/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/handler.tsx b/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/handler.tsx index 1252b04c072..bc82b7a8a8a 100644 --- a/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/handler.tsx +++ b/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/handler.tsx @@ -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 }) } } diff --git a/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/index.ts b/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/index.ts index c938810ebe1..5b8ac2daf85 100644 --- a/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/index.ts +++ b/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/index.ts @@ -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), +} diff --git a/examples/edge-functions/supabase/functions/openai-image-generation/index.ts b/examples/edge-functions/supabase/functions/openai-image-generation/index.ts index 0c5f46aaa3f..57dc6f618d6 100644 --- a/examples/edge-functions/supabase/functions/openai-image-generation/index.ts +++ b/examples/edge-functions/supabase/functions/openai-image-generation/index.ts @@ -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 } + ) + } + }), +} diff --git a/examples/edge-functions/supabase/functions/openai/index.ts b/examples/edge-functions/supabase/functions/openai/index.ts index f2e21ada5d5..a1c33f80140 100644 --- a/examples/edge-functions/supabase/functions/openai/index.ts +++ b/examples/edge-functions/supabase/functions/openai/index.ts @@ -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), + }) + }), +} diff --git a/examples/edge-functions/supabase/functions/opengraph/index.ts b/examples/edge-functions/supabase/functions/opengraph/index.ts index 59bf1335f84..fa547f03d0b 100644 --- a/examples/edge-functions/supabase/functions/opengraph/index.ts +++ b/examples/edge-functions/supabase/functions/opengraph/index.ts @@ -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), +} diff --git a/examples/edge-functions/supabase/functions/puppeteer/index.ts b/examples/edge-functions/supabase/functions/puppeteer/index.ts index 89e4188de7e..9455fa30ae0 100644 --- a/examples/edge-functions/supabase/functions/puppeteer/index.ts +++ b/examples/edge-functions/supabase/functions/puppeteer/index.ts @@ -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 }) + } + }), +} diff --git a/examples/edge-functions/supabase/functions/read-storage/index.ts b/examples/edge-functions/supabase/functions/read-storage/index.ts index d0063546b70..fb8d016e71d 100644 --- a/examples/edge-functions/supabase/functions/read-storage/index.ts +++ b/examples/edge-functions/supabase/functions/read-storage/index.ts @@ -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 }) + } + }), +} diff --git a/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts b/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts index c0c1e62b03c..2483900de6b 100644 --- a/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts +++ b/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts @@ -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 ' \ // --header 'Content-Type: application/json' \ // --data '{"name":"Functions"}' diff --git a/examples/edge-functions/supabase/functions/send-email-resend/index.ts b/examples/edge-functions/supabase/functions/send-email-resend/index.ts index 2326daa239a..16af83dd885 100644 --- a/examples/edge-functions/supabase/functions/send-email-resend/index.ts +++ b/examples/edge-functions/supabase/functions/send-email-resend/index.ts @@ -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 => { - 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: 'it works!', - }), - }) +// 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: 'it works!', + }), + }) - 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) diff --git a/examples/edge-functions/supabase/functions/send-email-smtp/index.ts b/examples/edge-functions/supabase/functions/send-email-smtp/index.ts index df778012853..669c3e8bf35 100644 --- a/examples/edge-functions/supabase/functions/send-email-smtp/index.ts +++ b/examples/edge-functions/supabase/functions/send-email-smtp/index.ts @@ -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((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((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 ' \ // --header 'Content-Type: application/json' \ // --data '{"name":"Functions"}' diff --git a/examples/edge-functions/supabase/functions/sentry/index.ts b/examples/edge-functions/supabase/functions/sentry/index.ts index 23a04024a92..6000568b6ac 100644 --- a/examples/edge-functions/supabase/functions/sentry/index.ts +++ b/examples/edge-functions/supabase/functions/sentry/index.ts @@ -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"}' diff --git a/examples/edge-functions/supabase/functions/sentryfied/index.ts b/examples/edge-functions/supabase/functions/sentryfied/index.ts index 4715db7a0d4..27cbc5895ba 100644 --- a/examples/edge-functions/supabase/functions/sentryfied/index.ts +++ b/examples/edge-functions/supabase/functions/sentryfied/index.ts @@ -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 }) + } + }), +} diff --git a/examples/edge-functions/supabase/functions/slack-bot-mention/index.ts b/examples/edge-functions/supabase/functions/slack-bot-mention/index.ts index d8eb337e8f2..428b6b86b18 100644 --- a/examples/edge-functions/supabase/functions/slack-bot-mention/index.ts +++ b/examples/edge-functions/supabase/functions/slack-bot-mention/index.ts @@ -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, - }) - } -}) + }), +} diff --git a/examples/edge-functions/supabase/functions/streams/index.ts b/examples/edge-functions/supabase/functions/streams/index.ts index 0680ac69c61..75123d6d452 100644 --- a/examples/edge-functions/supabase/functions/streams/index.ts +++ b/examples/edge-functions/supabase/functions/streams/index.ts @@ -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', + }, + }) + }), +} diff --git a/examples/edge-functions/supabase/functions/stripe-webhooks/index.ts b/examples/edge-functions/supabase/functions/stripe-webhooks/index.ts index fa78d5a5d21..b5048f2b6c8 100644 --- a/examples/edge-functions/supabase/functions/stripe-webhooks/index.ts +++ b/examples/edge-functions/supabase/functions/stripe-webhooks/index.ts @@ -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 }) + }), +} diff --git a/examples/edge-functions/supabase/functions/telegram-bot/index.ts b/examples/edge-functions/supabase/functions/telegram-bot/index.ts index 75333fa44c6..69aeb3239aa 100644 --- a/examples/edge-functions/supabase/functions/telegram-bot/index.ts +++ b/examples/edge-functions/supabase/functions/telegram-bot/index.ts @@ -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 }) + } + }), +} diff --git a/examples/edge-functions/supabase/functions/tweet-to-image/handler.tsx b/examples/edge-functions/supabase/functions/tweet-to-image/handler.tsx deleted file mode 100644 index 00e16086f49..00000000000 --- a/examples/edge-functions/supabase/functions/tweet-to-image/handler.tsx +++ /dev/null @@ -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( -
- -
, - { - 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, - }) - } -} diff --git a/examples/edge-functions/supabase/functions/tweet-to-image/index.ts b/examples/edge-functions/supabase/functions/tweet-to-image/index.ts index f2737b1981a..75c4d5d7653 100644 --- a/examples/edge-functions/supabase/functions/tweet-to-image/index.ts +++ b/examples/edge-functions/supabase/functions/tweet-to-image/index.ts @@ -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), +} diff --git a/examples/edge-functions/supabase/functions/upstash-redis-counter/index.ts b/examples/edge-functions/supabase/functions/upstash-redis-counter/index.ts index eb8d8be7e8e..ea111aca16b 100644 --- a/examples/edge-functions/supabase/functions/upstash-redis-counter/index.ts +++ b/examples/edge-functions/supabase/functions/upstash-redis-counter/index.ts @@ -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 | 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 | 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 ' diff --git a/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/index.ts b/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/index.ts index 0c8a05e10de..0c10eb3ead5 100644 --- a/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/index.ts +++ b/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/index.ts @@ -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 ' \ // --header 'Content-Type: application/json' \ // --data '{"name":"Functions"}' diff --git a/examples/edge-functions/supabase/functions/wasm-modules/index.ts b/examples/edge-functions/supabase/functions/wasm-modules/index.ts index 2d3bfa26125..e570899a6da 100644 --- a/examples/edge-functions/supabase/functions/wasm-modules/index.ts +++ b/examples/edge-functions/supabase/functions/wasm-modules/index.ts @@ -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) }) + }), +} diff --git a/examples/storage/protomaps/supabase/functions/maps-private/index.ts b/examples/storage/protomaps/supabase/functions/maps-private/index.ts index d0187cdafab..fb354884ffc 100644 --- a/examples/storage/protomaps/supabase/functions/maps-private/index.ts +++ b/examples/storage/protomaps/supabase/functions/maps-private/index.ts @@ -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 }) -}) diff --git a/examples/storage/resumable-upload-signed-uppy/supabase/config.toml b/examples/storage/resumable-upload-signed-uppy/supabase/config.toml index c54410aa307..285e516070f 100644 --- a/examples/storage/resumable-upload-signed-uppy/supabase/config.toml +++ b/examples/storage/resumable-upload-signed-uppy/supabase/config.toml @@ -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 diff --git a/examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/index.ts b/examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/index.ts index b61f9fafc6b..89ffb97f5a1 100644 --- a/examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/index.ts +++ b/examples/storage/resumable-upload-signed-uppy/supabase/functions/create-upload-token/index.ts @@ -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 }) - } -}) + }), +} diff --git a/examples/user-management/expo-push-notifications/supabase/config.toml b/examples/user-management/expo-push-notifications/supabase/config.toml index b75fd1de43a..ecd7f907b7a 100644 --- a/examples/user-management/expo-push-notifications/supabase/config.toml +++ b/examples/user-management/expo-push-notifications/supabase/config.toml @@ -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 diff --git a/examples/user-management/expo-push-notifications/supabase/functions/push/index.ts b/examples/user-management/expo-push-notifications/supabase/functions/push/index.ts index 76ea8f56338..5bc6acb78fc 100644 --- a/examples/user-management/expo-push-notifications/supabase/functions/push/index.ts +++ b/examples/user-management/expo-push-notifications/supabase/functions/push/index.ts @@ -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({ 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: ' \ // --header 'Content-Type: application/json' \ // --data '{"name":"Functions"}'