From 590a3148cdef79fdcc4b8f3916a85e38a79aeb30 Mon Sep 17 00:00:00 2001 From: developers-nightmare Date: Sun, 30 Nov 2025 13:33:13 +0530 Subject: [PATCH 1/4] Add dynamic auth token updates Adds update_auth/2 and :auth option for invoke/3 to support token rotation like JS client's setAuth(). Closes #3 --- CHANGELOG.md | 11 +++ README.md | 38 ++++++++ lib/supabase/functions.ex | 45 +++++++++- test/supabase/functions_test.exs | 143 +++++++++++++++++++++++++++++++ 4 files changed, 236 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29..35bdca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +## [Unreleased] + +### Added +- Dynamic auth token updates feature for better token rotation support + - `Supabase.Functions.update_auth/2` function for functional client updates + - `:auth` option in `Supabase.Functions.invoke/3` for per-request auth token overrides + - Feature parity with JavaScript client's `setAuth(token)` method + - Comprehensive test coverage for both update methods +- Enhanced documentation with examples for both auth update approaches diff --git a/README.md b/README.md index b5f7d71..fcb602f 100644 --- a/README.md +++ b/README.md @@ -124,3 +124,41 @@ This feature provides: - **Better resource management**: Prevents hanging connections - **Comprehensive timeout coverage**: Sets both receive timeout (per-chunk) and request timeout (complete response) - **Feature parity with JS client**: Matches timeout functionality in the JavaScript SDK + +## Dynamic Auth Token Updates + +You can update authorization tokens dynamically using two approaches, providing feature parity with the JavaScript client's `setAuth(token)` method: + +### Option 1: Functional Update + +Create a new client with an updated auth token: + +```elixir +client = Supabase.init_client!("SUPABASE_URL", "SUPABASE_KEY") + +# Update auth token functionally +updated_client = Supabase.Functions.update_auth(client, "new_jwt_token") +{:ok, response} = Supabase.Functions.invoke(updated_client, "my-function") +``` + +### Option 2: Per-Request Override + +Override the authorization token for a specific request: + +```elixir +client = Supabase.init_client!("SUPABASE_URL", "SUPABASE_KEY") + +# Use a different token for this request only +{:ok, response} = Supabase.Functions.invoke(client, "my-function", auth: "user_jwt_token") + +# Original client remains unchanged for subsequent calls +{:ok, response2} = Supabase.Functions.invoke(client, "another-function") +``` + +### Benefits + +- **Token rotation support**: Easily update tokens without recreating clients +- **Better performance**: Avoid the overhead of creating new client instances +- **Flexibility**: Use different tokens per request or update client globally +- **Feature parity**: Matches the JavaScript client's `setAuth()` functionality +- **Immutability**: Original client instances remain unchanged with functional updates diff --git a/lib/supabase/functions.ex b/lib/supabase/functions.ex index 01c1a28..854a81f 100644 --- a/lib/supabase/functions.ex +++ b/lib/supabase/functions.ex @@ -25,6 +25,7 @@ defmodule Supabase.Functions do - `region`: The Region to invoke the function in. - `on_response`: The custom response handler for response streaming. - `timeout`: The timeout in milliseconds for the request. Defaults to 15 seconds. + - `auth`: Override the authorization token for this request. """ @type opt :: {:body, Fetcher.body()} @@ -33,6 +34,7 @@ defmodule Supabase.Functions do | {:region, region} | {:on_response, on_response} | {:timeout, pos_integer()} + | {:auth, String.t()} @type on_response :: ({Fetcher.status(), Fetcher.headers(), body :: Enumerable.t()} -> Supabase.result(Response.t())) @@ -54,6 +56,24 @@ defmodule Supabase.Functions do | :"ca-central-1" | :"eu-central-1" + @doc """ + Updates the access token for a client + + Creates a new client instance with the updated access token. This provides + feature parity with the JavaScript client's `setAuth(token)` method. + + ## Examples + + # Update auth token functionally + new_client = Supabase.Functions.update_auth(client, "new_token") + {:ok, response} = Supabase.Functions.invoke(new_client, "my-function") + + """ + @spec update_auth(Client.t(), String.t()) :: Client.t() + def update_auth(%Client{} = client, token) when is_binary(token) do + %{client | access_token: token} + end + @doc """ Invokes a function @@ -62,6 +82,19 @@ defmodule Supabase.Functions do - When you pass in a body to your function, we automatically attach the `Content-Type` header automatically. If it doesn't match any of these types we assume the payload is json, serialize it and attach the `Content-Type` header as `application/json`. You can override this behavior by passing in a `Content-Type` header of your own. - Responses are automatically parsed as json depending on the Content-Type header sent by your function. Responses are parsed as text by default. + ## Authentication + + You can override the authorization token for a specific request using the `auth` option: + + # Use a different token for this request + {:ok, response} = Supabase.Functions.invoke(client, "my-function", auth: "new_token") + + Alternatively, you can update the client's auth token functionally: + + # Update the client's token + new_client = Supabase.Functions.update_auth(client, "new_token") + {:ok, response} = Supabase.Functions.invoke(new_client, "my-function") + ## Timeout Support You can set a timeout for function invocations using the `timeout` option. This sets both the @@ -84,6 +117,9 @@ defmodule Supabase.Functions do {:ok, response} = Supabase.Functions.invoke(client, "my-function", body: %{data: "value"}, timeout: 30_000) + + # With custom auth token + {:ok, response} = Supabase.Functions.invoke(client, "my-function", auth: "custom_token") """ @spec invoke(Client.t(), function :: String.t(), opts) :: Supabase.result(Response.t()) def invoke(%Client{} = client, name, opts \\ []) when is_binary(name) do @@ -91,7 +127,14 @@ defmodule Supabase.Functions do custom_headers = opts[:headers] || %{} timeout = opts[:timeout] || 15_000 - client + # Handle auth token override + effective_client = + case opts[:auth] do + nil -> client + auth_token when is_binary(auth_token) -> update_auth(client, auth_token) + end + + effective_client |> Request.new(decode_body?: false) |> Request.with_functions_url(name) |> Request.with_method(method) diff --git a/test/supabase/functions_test.exs b/test/supabase/functions_test.exs index a619ff5..1667b2a 100644 --- a/test/supabase/functions_test.exs +++ b/test/supabase/functions_test.exs @@ -257,4 +257,147 @@ defmodule Supabase.FunctionsTest do assert response == "chunk1chunk2" end end + + describe "update_auth/2" do + test "returns a new client with updated access token", %{client: client} do + new_token = "new_test_token" + updated_client = Functions.update_auth(client, new_token) + + assert updated_client.access_token == new_token + # Original client should remain unchanged + assert client.access_token != new_token + # All other fields should remain the same + assert updated_client.base_url == client.base_url + assert updated_client.api_key == client.api_key + end + + test "updated client works with invoke/3", %{client: client} do + new_token = "updated_token" + + expect(@mock, :stream, fn request, _opts -> + assert Request.get_header(request, "authorization") == "Bearer #{new_token}" + + {:ok, + %Finch.Response{ + status: 200, + headers: %{"content-type" => "application/json"}, + body: ~s({"success": true}) + }} + end) + + updated_client = Functions.update_auth(client, new_token) + + assert {:ok, response} = + Functions.invoke(updated_client, "test-function", http_client: @mock) + + assert response.body == %{"success" => true} + end + end + + describe "auth option in invoke/3" do + test "overrides authorization header with custom token", %{client: client} do + custom_token = "custom_auth_token" + + expect(@mock, :stream, fn request, _opts -> + assert Request.get_header(request, "authorization") == "Bearer #{custom_token}" + + {:ok, + %Finch.Response{ + status: 200, + headers: %{"content-type" => "application/json"}, + body: ~s({"authorized": true}) + }} + end) + + assert {:ok, response} = + Functions.invoke(client, "test-function", auth: custom_token, http_client: @mock) + + assert response.body == %{"authorized" => true} + end + + test "works with other options combined", %{client: client} do + custom_token = "combined_auth_token" + custom_headers = %{"x-custom" => "value"} + body_data = %{test: "data"} + + expect(@mock, :stream, fn request, _opts -> + assert Request.get_header(request, "authorization") == "Bearer #{custom_token}" + assert Request.get_header(request, "x-custom") == "value" + assert Request.get_header(request, "content-type") == "application/json" + + {:ok, + %Finch.Response{ + status: 200, + headers: %{"content-type" => "application/json"}, + body: ~s({"success": true}) + }} + end) + + assert {:ok, response} = + Functions.invoke(client, "test-function", + auth: custom_token, + headers: custom_headers, + body: body_data, + http_client: @mock + ) + + assert response.body == %{"success" => true} + end + + test "original client remains unchanged after auth override", %{client: client} do + original_token = client.access_token + custom_token = "temporary_override_token" + + expect(@mock, :stream, fn request, _opts -> + assert Request.get_header(request, "authorization") == "Bearer #{custom_token}" + + {:ok, + %Finch.Response{ + status: 200, + headers: %{"content-type" => "application/json"}, + body: ~s({"success": true}) + }} + end) + + assert {:ok, _response} = + Functions.invoke(client, "test-function", auth: custom_token, http_client: @mock) + + # Original client should be unchanged + assert client.access_token == original_token + end + + test "nil auth option uses original client token", %{client: client} do + expect(@mock, :stream, fn request, _opts -> + assert Request.get_header(request, "authorization") == "Bearer #{client.access_token}" + + {:ok, + %Finch.Response{ + status: 200, + headers: %{"content-type" => "application/json"}, + body: ~s({"success": true}) + }} + end) + + assert {:ok, response} = + Functions.invoke(client, "test-function", auth: nil, http_client: @mock) + + assert response.body == %{"success" => true} + end + + test "empty auth option uses original client token", %{client: client} do + expect(@mock, :stream, fn request, _opts -> + assert Request.get_header(request, "authorization") == "Bearer #{client.access_token}" + + {:ok, + %Finch.Response{ + status: 200, + headers: %{"content-type" => "application/json"}, + body: ~s({"success": true}) + }} + end) + + assert {:ok, response} = Functions.invoke(client, "test-function", http_client: @mock) + assert response.body == %{"success" => true} + end + end end From c3ba82d098c01fc2714929229ca7dcdb818b8cdf Mon Sep 17 00:00:00 2001 From: developers-nightmare Date: Tue, 2 Dec 2025 14:33:19 +0530 Subject: [PATCH 2/4] all done --- README.md | 39 -------------------------------- lib/supabase/functions.ex | 20 ++++++++-------- test/supabase/functions_test.exs | 16 ++++++------- 3 files changed, 18 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index fcb602f..2e17d72 100644 --- a/README.md +++ b/README.md @@ -123,42 +123,3 @@ This feature provides: - **Request cancellation**: Long-running requests will timeout and be cancelled - **Better resource management**: Prevents hanging connections - **Comprehensive timeout coverage**: Sets both receive timeout (per-chunk) and request timeout (complete response) -- **Feature parity with JS client**: Matches timeout functionality in the JavaScript SDK - -## Dynamic Auth Token Updates - -You can update authorization tokens dynamically using two approaches, providing feature parity with the JavaScript client's `setAuth(token)` method: - -### Option 1: Functional Update - -Create a new client with an updated auth token: - -```elixir -client = Supabase.init_client!("SUPABASE_URL", "SUPABASE_KEY") - -# Update auth token functionally -updated_client = Supabase.Functions.update_auth(client, "new_jwt_token") -{:ok, response} = Supabase.Functions.invoke(updated_client, "my-function") -``` - -### Option 2: Per-Request Override - -Override the authorization token for a specific request: - -```elixir -client = Supabase.init_client!("SUPABASE_URL", "SUPABASE_KEY") - -# Use a different token for this request only -{:ok, response} = Supabase.Functions.invoke(client, "my-function", auth: "user_jwt_token") - -# Original client remains unchanged for subsequent calls -{:ok, response2} = Supabase.Functions.invoke(client, "another-function") -``` - -### Benefits - -- **Token rotation support**: Easily update tokens without recreating clients -- **Better performance**: Avoid the overhead of creating new client instances -- **Flexibility**: Use different tokens per request or update client globally -- **Feature parity**: Matches the JavaScript client's `setAuth()` functionality -- **Immutability**: Original client instances remain unchanged with functional updates diff --git a/lib/supabase/functions.ex b/lib/supabase/functions.ex index 854a81f..8309806 100644 --- a/lib/supabase/functions.ex +++ b/lib/supabase/functions.ex @@ -25,7 +25,7 @@ defmodule Supabase.Functions do - `region`: The Region to invoke the function in. - `on_response`: The custom response handler for response streaming. - `timeout`: The timeout in milliseconds for the request. Defaults to 15 seconds. - - `auth`: Override the authorization token for this request. + - `access_token`: Override the authorization token for this request. """ @type opt :: {:body, Fetcher.body()} @@ -34,7 +34,7 @@ defmodule Supabase.Functions do | {:region, region} | {:on_response, on_response} | {:timeout, pos_integer()} - | {:auth, String.t()} + | {:access_token, String.t()} @type on_response :: ({Fetcher.status(), Fetcher.headers(), body :: Enumerable.t()} -> Supabase.result(Response.t())) @@ -84,10 +84,10 @@ defmodule Supabase.Functions do ## Authentication - You can override the authorization token for a specific request using the `auth` option: + You can override the authorization token for a specific request using the `access_token` option: # Use a different token for this request - {:ok, response} = Supabase.Functions.invoke(client, "my-function", auth: "new_token") + {:ok, response} = Supabase.Functions.invoke(client, "my-function", access_token: "new_token") Alternatively, you can update the client's auth token functionally: @@ -118,8 +118,8 @@ defmodule Supabase.Functions do body: %{data: "value"}, timeout: 30_000) - # With custom auth token - {:ok, response} = Supabase.Functions.invoke(client, "my-function", auth: "custom_token") + # With custom access token + {:ok, response} = Supabase.Functions.invoke(client, "my-function", access_token: "custom_token") """ @spec invoke(Client.t(), function :: String.t(), opts) :: Supabase.result(Response.t()) def invoke(%Client{} = client, name, opts \\ []) when is_binary(name) do @@ -127,11 +127,11 @@ defmodule Supabase.Functions do custom_headers = opts[:headers] || %{} timeout = opts[:timeout] || 15_000 - # Handle auth token override effective_client = - case opts[:auth] do - nil -> client - auth_token when is_binary(auth_token) -> update_auth(client, auth_token) + if opts[:access_token] do + update_auth(client, opts[:access_token]) + else + client end effective_client diff --git a/test/supabase/functions_test.exs b/test/supabase/functions_test.exs index 1667b2a..524c912 100644 --- a/test/supabase/functions_test.exs +++ b/test/supabase/functions_test.exs @@ -294,7 +294,7 @@ defmodule Supabase.FunctionsTest do end end - describe "auth option in invoke/3" do + describe "access_token option in invoke/3" do test "overrides authorization header with custom token", %{client: client} do custom_token = "custom_auth_token" @@ -310,7 +310,7 @@ defmodule Supabase.FunctionsTest do end) assert {:ok, response} = - Functions.invoke(client, "test-function", auth: custom_token, http_client: @mock) + Functions.invoke(client, "test-function", access_token: custom_token, http_client: @mock) assert response.body == %{"authorized" => true} end @@ -335,7 +335,7 @@ defmodule Supabase.FunctionsTest do assert {:ok, response} = Functions.invoke(client, "test-function", - auth: custom_token, + access_token: custom_token, headers: custom_headers, body: body_data, http_client: @mock @@ -344,7 +344,7 @@ defmodule Supabase.FunctionsTest do assert response.body == %{"success" => true} end - test "original client remains unchanged after auth override", %{client: client} do + test "original client remains unchanged after access_token override", %{client: client} do original_token = client.access_token custom_token = "temporary_override_token" @@ -360,13 +360,13 @@ defmodule Supabase.FunctionsTest do end) assert {:ok, _response} = - Functions.invoke(client, "test-function", auth: custom_token, http_client: @mock) + Functions.invoke(client, "test-function", access_token: custom_token, http_client: @mock) # Original client should be unchanged assert client.access_token == original_token end - test "nil auth option uses original client token", %{client: client} do + test "nil access_token option uses original client token", %{client: client} do expect(@mock, :stream, fn request, _opts -> assert Request.get_header(request, "authorization") == "Bearer #{client.access_token}" @@ -379,12 +379,12 @@ defmodule Supabase.FunctionsTest do end) assert {:ok, response} = - Functions.invoke(client, "test-function", auth: nil, http_client: @mock) + Functions.invoke(client, "test-function", access_token: nil, http_client: @mock) assert response.body == %{"success" => true} end - test "empty auth option uses original client token", %{client: client} do + test "empty access_token option uses original client token", %{client: client} do expect(@mock, :stream, fn request, _opts -> assert Request.get_header(request, "authorization") == "Bearer #{client.access_token}" From 4a2cb31b7aa6574138834f8cba0fde1c052db457 Mon Sep 17 00:00:00 2001 From: developers-nightmare Date: Tue, 2 Dec 2025 14:41:42 +0530 Subject: [PATCH 3/4] fixed lint errors --- test/supabase/functions_test.exs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/supabase/functions_test.exs b/test/supabase/functions_test.exs index 524c912..3e17d66 100644 --- a/test/supabase/functions_test.exs +++ b/test/supabase/functions_test.exs @@ -310,7 +310,10 @@ defmodule Supabase.FunctionsTest do end) assert {:ok, response} = - Functions.invoke(client, "test-function", access_token: custom_token, http_client: @mock) + Functions.invoke(client, "test-function", + access_token: custom_token, + http_client: @mock + ) assert response.body == %{"authorized" => true} end @@ -360,7 +363,10 @@ defmodule Supabase.FunctionsTest do end) assert {:ok, _response} = - Functions.invoke(client, "test-function", access_token: custom_token, http_client: @mock) + Functions.invoke(client, "test-function", + access_token: custom_token, + http_client: @mock + ) # Original client should be unchanged assert client.access_token == original_token From c0be90af91715a41d1ead248a6729ae0b7c75f24 Mon Sep 17 00:00:00 2001 From: prathish D Date: Tue, 2 Dec 2025 23:05:40 +0530 Subject: [PATCH 4/4] Delete CHANGELOG.md --- CHANGELOG.md | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 35bdca8..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,11 +0,0 @@ -# Changelog - -## [Unreleased] - -### Added -- Dynamic auth token updates feature for better token rotation support - - `Supabase.Functions.update_auth/2` function for functional client updates - - `:auth` option in `Supabase.Functions.invoke/3` for per-request auth token overrides - - Feature parity with JavaScript client's `setAuth(token)` method - - Comprehensive test coverage for both update methods -- Enhanced documentation with examples for both auth update approaches