aboutsummaryrefslogtreecommitdiff
path: root/supabase
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-03 23:41:22 -0800
committerFuwn <[email protected]>2026-02-04 00:29:28 -0800
commit3ae36eeadd8b1f6287ed55060b46ce82947b56b7 (patch)
tree51b0183580aaf12ccb80c0792cc5b185994179ce /supabase
parentstyle: Organise imports across packages (diff)
downloadarchived-imemio-3ae36eeadd8b1f6287ed55060b46ce82947b56b7.tar.xz
archived-imemio-3ae36eeadd8b1f6287ed55060b46ce82947b56b7.zip
feat: Add API key authentication for MCP server
Diffstat (limited to 'supabase')
-rw-r--r--supabase/functions/validate-api-key/index.ts73
-rw-r--r--supabase/migrations/20260204000001_initial_schema.sql (renamed from supabase/migrations/001_initial_schema.sql)0
-rw-r--r--supabase/migrations/20260204000002_variable_embedding_dimensions.sql (renamed from supabase/migrations/002_variable_embedding_dimensions.sql)0
-rw-r--r--supabase/migrations/20260204000003_api_keys.sql64
4 files changed, 137 insertions, 0 deletions
diff --git a/supabase/functions/validate-api-key/index.ts b/supabase/functions/validate-api-key/index.ts
new file mode 100644
index 0000000..d426b7e
--- /dev/null
+++ b/supabase/functions/validate-api-key/index.ts
@@ -0,0 +1,73 @@
+import { createClient } from "jsr:@supabase/supabase-js@2";
+
+const corsHeaders = {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Headers":
+ "authorization, x-client-info, apikey, content-type",
+};
+
+Deno.serve(async (request) => {
+ if (request.method === "OPTIONS") {
+ return new Response("ok", { headers: corsHeaders });
+ }
+
+ try {
+ const { apiKey } = await request.json();
+
+ if (!apiKey || !apiKey.startsWith("imemio_")) {
+ return new Response(JSON.stringify({ error: "Invalid API key format" }), {
+ status: 400,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ });
+ }
+
+ const encoder = new TextEncoder();
+ const data = encoder.encode(apiKey);
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const keyHash = hashArray
+ .map((byte) => byte.toString(16).padStart(2, "0"))
+ .join("");
+ const supabaseUrl = Deno.env.get("SUPABASE_URL");
+ const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
+
+ if (!supabaseUrl || !supabaseServiceKey) {
+ return new Response(
+ JSON.stringify({ error: "Server configuration error" }),
+ {
+ status: 500,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ const supabase = createClient(supabaseUrl, supabaseServiceKey);
+ const { data: result, error } = await supabase.rpc("validate_api_key", {
+ api_key_hash: keyHash,
+ });
+
+ if (error || !result || result.length === 0) {
+ return new Response(
+ JSON.stringify({ error: "Invalid or revoked API key" }),
+ {
+ status: 401,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ const { user_id: userId, api_key_id: apiKeyId } = result[0];
+
+ supabase.rpc("update_api_key_last_used", { p_api_key_id: apiKeyId });
+
+ return new Response(JSON.stringify({ userId, apiKeyId }), {
+ status: 200,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ });
+ } catch {
+ return new Response(JSON.stringify({ error: "Internal server error" }), {
+ status: 500,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ });
+ }
+});
diff --git a/supabase/migrations/001_initial_schema.sql b/supabase/migrations/20260204000001_initial_schema.sql
index 09b028e..09b028e 100644
--- a/supabase/migrations/001_initial_schema.sql
+++ b/supabase/migrations/20260204000001_initial_schema.sql
diff --git a/supabase/migrations/002_variable_embedding_dimensions.sql b/supabase/migrations/20260204000002_variable_embedding_dimensions.sql
index 522df47..522df47 100644
--- a/supabase/migrations/002_variable_embedding_dimensions.sql
+++ b/supabase/migrations/20260204000002_variable_embedding_dimensions.sql
diff --git a/supabase/migrations/20260204000003_api_keys.sql b/supabase/migrations/20260204000003_api_keys.sql
new file mode 100644
index 0000000..515deb4
--- /dev/null
+++ b/supabase/migrations/20260204000003_api_keys.sql
@@ -0,0 +1,64 @@
+create table public.api_keys (
+ id uuid primary key default gen_random_uuid(),
+ user_id uuid not null references public.users(id) on delete cascade,
+ name text not null,
+ key_prefix text not null,
+ key_hash text not null unique,
+ last_used_at timestamptz,
+ expires_at timestamptz,
+ revoked_at timestamptz,
+ created_at timestamptz not null default now(),
+ updated_at timestamptz not null default now()
+);
+
+create index api_keys_user_id_idx on public.api_keys(user_id);
+create index api_keys_key_hash_idx on public.api_keys(key_hash);
+
+create trigger api_keys_updated_at
+ before update on public.api_keys
+ for each row execute function public.handle_updated_at();
+
+alter table public.api_keys enable row level security;
+
+create policy "Users can view own API keys"
+ on public.api_keys for select
+ using (auth.uid() = user_id);
+
+create policy "Users can create own API keys"
+ on public.api_keys for insert
+ with check (auth.uid() = user_id);
+
+create policy "Users can update own API keys"
+ on public.api_keys for update
+ using (auth.uid() = user_id);
+
+create policy "Users can delete own API keys"
+ on public.api_keys for delete
+ using (auth.uid() = user_id);
+
+create or replace function public.validate_api_key(api_key_hash text)
+returns table (user_id uuid, api_key_id uuid)
+language plpgsql
+security definer
+as $$
+begin
+ return query
+ select ak.user_id, ak.id as api_key_id
+ from public.api_keys ak
+ where ak.key_hash = api_key_hash
+ and ak.revoked_at is null
+ and (ak.expires_at is null or ak.expires_at > now());
+end;
+$$;
+
+create or replace function public.update_api_key_last_used(p_api_key_id uuid)
+returns void
+language plpgsql
+security definer
+as $$
+begin
+ update public.api_keys
+ set last_used_at = now()
+ where id = p_api_key_id;
+end;
+$$;