From 8f67753421727618f2b09fe82deff23f5d3ef5f7 Mon Sep 17 00:00:00 2001 From: Kalleby Santos Date: Fri, 10 Apr 2026 19:40:23 +0100 Subject: [PATCH 1/4] feat: creating auth access templates - Creating different templates for 'functions new' command - Handling '--auth' flag to specify which template use --- cmd/functions.go | 15 ++++- internal/functions/new/new.go | 65 ++++++++++++++----- internal/functions/new/templates/config.toml | 10 +-- internal/functions/new/templates/deno.json | 3 +- .../new/templates/index_always_access.ts | 32 +++++++++ .../new/templates/index_apikey_access.ts | 46 +++++++++++++ .../{index.ts => index_user_access.ts} | 26 ++++---- 7 files changed, 159 insertions(+), 38 deletions(-) create mode 100644 internal/functions/new/templates/index_always_access.ts create mode 100644 internal/functions/new/templates/index_apikey_access.ts rename internal/functions/new/templates/{index.ts => index_user_access.ts} (57%) diff --git a/cmd/functions.go b/cmd/functions.go index 72ea09da0b..9c6575494d 100644 --- a/cmd/functions.go +++ b/cmd/functions.go @@ -85,6 +85,13 @@ var ( }, } + authAccessMode = utils.EnumFlag{ + Allowed: []string{ + string(new_.AuthAccessModeAlways), + string(new_.AuthAccessModeApiKey), + string(new_.AuthAccessModeUser), + }, + } functionsNewCmd = &cobra.Command{ Use: "new ", Short: "Create a new Function locally", @@ -94,7 +101,12 @@ var ( return cmd.Root().PersistentPreRunE(cmd, args) }, RunE: func(cmd *cobra.Command, args []string) error { - return new_.Run(cmd.Context(), args[0], afero.NewOsFs()) + authMode := new_.AuthAccessModeAlways + if len(authAccessMode.Value) > 0 { + authMode = new_.AuthAccessMode(authAccessMode.Value) + } + + return new_.Run(cmd.Context(), args[0], authMode, afero.NewOsFs()) }, } @@ -168,6 +180,7 @@ func init() { functionsDownloadCmd.MarkFlagsMutuallyExclusive("use-api", "use-docker", "legacy-bundle") cobra.CheckErr(downloadFlags.MarkHidden("legacy-bundle")) cobra.CheckErr(downloadFlags.MarkHidden("use-docker")) + functionsNewCmd.Flags().Var(&authAccessMode, "auth", "use a specific auth access (default always)") functionsCmd.AddCommand(functionsListCmd) functionsCmd.AddCommand(functionsDeleteCmd) functionsCmd.AddCommand(functionsDeployCmd) diff --git a/internal/functions/new/new.go b/internal/functions/new/new.go index 1b0f307912..e6045a956d 100644 --- a/internal/functions/new/new.go +++ b/internal/functions/new/new.go @@ -16,9 +16,22 @@ import ( "github.com/supabase/cli/internal/utils/flags" ) +type AuthAccessMode string + +const ( + AuthAccessModeAlways AuthAccessMode = "always" + AuthAccessModeApiKey AuthAccessMode = "apikey" + AuthAccessModeUser AuthAccessMode = "user" +) + var ( - //go:embed templates/index.ts - indexEmbed string + //go:embed templates/index_always_access.ts + indexAuthAlwaysEmbed string + //go:embed templates/index_apikey_access.ts + indexAuthApiKeyEmbed string + //go:embed templates/index_user_access.ts + indexAuthUserEmbed string + //go:embed templates/deno.json denoEmbed string //go:embed templates/.npmrc @@ -26,16 +39,26 @@ var ( //go:embed templates/config.toml configEmbed string - indexTemplate = template.Must(template.New("index").Parse(indexEmbed)) + indexAuthTemplates = map[AuthAccessMode]*template.Template{ + AuthAccessModeAlways: template.Must(template.New("index").Parse(indexAuthAlwaysEmbed)), + AuthAccessModeApiKey: template.Must(template.New("index").Parse(indexAuthApiKeyEmbed)), + AuthAccessModeUser: template.Must(template.New("index").Parse(indexAuthUserEmbed)), + } + configTemplate = template.Must(template.New("config").Parse(configEmbed)) ) type indexConfig struct { - URL string - Token string + URL string + PublishableKey string +} + +type functionConfig struct { + Slug string + VerifyJWT bool } -func Run(ctx context.Context, slug string, fsys afero.Fs) error { +func Run(ctx context.Context, slug string, authMode AuthAccessMode, fsys afero.Fs) error { // 1. Sanity checks. if err := utils.ValidateFunctionSlug(slug); err != nil { return err @@ -56,17 +79,18 @@ func Run(ctx context.Context, slug string, fsys afero.Fs) error { if err := flags.LoadConfig(fsys); err != nil { fmt.Fprintln(utils.GetDebugLogger(), err) } - if err := createEntrypointFile(slug, fsys); err != nil { + if err := createEntrypointFile(slug, authMode, fsys); err != nil { return err } - if err := appendConfigFile(slug, fsys); err != nil { + verifyJWT := authMode == AuthAccessModeUser + if err := appendConfigFile(slug, verifyJWT, fsys); err != nil { return err } // 3. Create optional files - if err := afero.WriteFile(fsys, filepath.Join(funcDir, "deno.json"), []byte(denoEmbed), 0644); err != nil { + if err := afero.WriteFile(fsys, filepath.Join(funcDir, "deno.json"), []byte(denoEmbed), 0o644); err != nil { return errors.Errorf("failed to create deno.json config: %w", err) } - if err := afero.WriteFile(fsys, filepath.Join(funcDir, ".npmrc"), []byte(npmrcEmbed), 0644); err != nil { + if err := afero.WriteFile(fsys, filepath.Join(funcDir, ".npmrc"), []byte(npmrcEmbed), 0o644); err != nil { return errors.Errorf("failed to create .npmrc config: %w", err) } fmt.Println("Created new Function at " + utils.Bold(funcDir)) @@ -79,33 +103,40 @@ func Run(ctx context.Context, slug string, fsys afero.Fs) error { return nil } -func createEntrypointFile(slug string, fsys afero.Fs) error { +func createEntrypointFile(slug string, authMode AuthAccessMode, fsys afero.Fs) error { entrypointPath := filepath.Join(utils.FunctionsDir, slug, "index.ts") - f, err := fsys.OpenFile(entrypointPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) + f, err := fsys.OpenFile(entrypointPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) if err != nil { return errors.Errorf("failed to create entrypoint: %w", err) } defer f.Close() + indexTemplate, hasTemplate := indexAuthTemplates[authMode] + if !hasTemplate { + return errors.Errorf("failed to write entrypoint: '%w' is not a valid template", authMode) + } if err := indexTemplate.Option("missingkey=error").Execute(f, indexConfig{ - URL: utils.GetApiUrl("/functions/v1/" + slug), - Token: utils.Config.Auth.AnonKey.Value, + URL: utils.GetApiUrl("/functions/v1/" + slug), + PublishableKey: utils.Config.Auth.PublishableKey.Value, }); err != nil { return errors.Errorf("failed to write entrypoint: %w", err) } return nil } -func appendConfigFile(slug string, fsys afero.Fs) error { +func appendConfigFile(slug string, verifyJWT bool, fsys afero.Fs) error { if _, exists := utils.Config.Functions[slug]; exists { fmt.Fprintf(os.Stderr, "[functions.%s] is already declared in %s\n", slug, utils.Bold(utils.ConfigPath)) return nil } - f, err := fsys.OpenFile(utils.ConfigPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + f, err := fsys.OpenFile(utils.ConfigPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644) if err != nil { return errors.Errorf("failed to append config: %w", err) } defer f.Close() - if err := configTemplate.Option("missingkey=error").Execute(f, slug); err != nil { + if err := configTemplate.Option("missingkey=error").Execute(f, functionConfig{ + Slug: slug, + VerifyJWT: verifyJWT, + }); err != nil { return errors.Errorf("failed to append template: %w", err) } return nil diff --git a/internal/functions/new/templates/config.toml b/internal/functions/new/templates/config.toml index 9b3b197098..30f63a70ee 100644 --- a/internal/functions/new/templates/config.toml +++ b/internal/functions/new/templates/config.toml @@ -1,11 +1,11 @@ -[functions.{{ . }}] +[functions.{{ .Slug }}] enabled = true -verify_jwt = true -import_map = "./functions/{{ . }}/deno.json" +verify_jwt = {{ .VerifyJWT }} +import_map = "./functions/{{ .Slug }}/deno.json" # Uncomment to specify a custom file path to the entrypoint. # Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx -entrypoint = "./functions/{{ . }}/index.ts" +entrypoint = "./functions/{{ .Slug }}/index.ts" # Specifies static files to be bundled with the function. Supports glob patterns. # For example, if you want to serve static HTML pages in your function: -# static_files = [ "./functions/{{ . }}/*.html" ] +# static_files = [ "./functions/{{ .Slug }}/*.html" ] diff --git a/internal/functions/new/templates/deno.json b/internal/functions/new/templates/deno.json index 758d0703d1..40fd1e8b28 100644 --- a/internal/functions/new/templates/deno.json +++ b/internal/functions/new/templates/deno.json @@ -1,5 +1,6 @@ { "imports": { - "@supabase/functions-js": "jsr:@supabase/functions-js@^2" + "@supabase/functions-js": "jsr:@supabase/functions-js@^2", + "@supabase/server": "npm:@supabase/server" } } diff --git a/internal/functions/new/templates/index_always_access.ts b/internal/functions/new/templates/index_always_access.ts new file mode 100644 index 0000000000..5f47542408 --- /dev/null +++ b/internal/functions/new/templates/index_always_access.ts @@ -0,0 +1,32 @@ +// 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. + +// Setup type definitions for built-in Supabase Runtime APIs +import "@supabase/functions-js/edge-runtime.d.ts"; +import { withSupabase } from "@supabase/server"; + +console.log("Hello from Functions!"); + +// This endpoint uses 'always' access, no credentials required, every request is accepted. +// Use it for health checks, public APIs, or when you handle auth yourself inside the handler. +export default { + fetch: withSupabase({ allow: "always" }, async (req, ctx) => { + const { name } = await req.json(); + + return Response.json({ + message: `Hello ${name}!`, + }); + }), +}; + +/* To invoke locally: + + 1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start) + 2. Make an HTTP request: + + curl -i --location --request POST '{{ .URL }}' \ + --header 'Content-Type: application/json' \ + --data '{"name":"Functions"}' + +*/ diff --git a/internal/functions/new/templates/index_apikey_access.ts b/internal/functions/new/templates/index_apikey_access.ts new file mode 100644 index 0000000000..cfb9acc91d --- /dev/null +++ b/internal/functions/new/templates/index_apikey_access.ts @@ -0,0 +1,46 @@ +// 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. + +// Setup type definitions for built-in Supabase Runtime APIs +import "@supabase/functions-js/edge-runtime.d.ts"; +import { withSupabase } from "@supabase/server"; + +console.log("Hello from Functions!"); + +// This endpoint uses 'public' | 'secret' access, apiKey is required. +// Use public for Client-facing, key-validated endpoints +// Use secret for Server-to-server, internal calls +export default { + fetch: withSupabase({ allow: ["public", "secret"] }, async (req, ctx) => { + // Called by another service with a secret key + // ctx.supabaseAdmin bypasses RLS — use for privileged operations + /* + if (ctx.authType === "secret") { + const { user_id } = await req.json(); + const { data } = await ctx.supabaseAdmin.auth.admin.getUserById(user_id); + + return Response.json({ + email: data?.user?.email, + }); + } + */ + + const { name } = await req.json(); + + return Response.json({ + message: `Hello ${name}!`, + }); + }), +}; + +/* To invoke locally: + + 1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start) + 2. Make an HTTP request: + + curl -i --location --request POST '{{ .URL }}' \ + --header 'apiKey: {{ .PublishableKey }}' \ + --data '{"name":"Functions"}' + +*/ diff --git a/internal/functions/new/templates/index.ts b/internal/functions/new/templates/index_user_access.ts similarity index 57% rename from internal/functions/new/templates/index.ts rename to internal/functions/new/templates/index_user_access.ts index dd1f7ce676..0e0c71c4aa 100644 --- a/internal/functions/new/templates/index.ts +++ b/internal/functions/new/templates/index_user_access.ts @@ -4,20 +4,20 @@ // Setup type definitions for built-in Supabase Runtime APIs import "@supabase/functions-js/edge-runtime.d.ts" +import { withSupabase } from '@supabase/server' console.log("Hello from Functions!") -Deno.serve(async (req) => { - const { name } = await req.json() - const data = { - message: `Hello ${name}!`, - } +// This endpoint uses 'user' access, credentials is required. +export default { + fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => { + const email = ctx.userClaims?.email; - return new Response( - JSON.stringify(data), - { headers: { "Content-Type": "application/json" } }, - ) -}) + return Response.json({ + message: `Hello ${email}!`, + }) + }), +} /* To invoke locally: @@ -25,8 +25,6 @@ Deno.serve(async (req) => { 2. Make an HTTP request: curl -i --location --request POST '{{ .URL }}' \ - --header 'Authorization: Bearer {{ .Token }}' \ - --header 'Content-Type: application/json' \ - --data '{"name":"Functions"}' - + --header 'apiKey: {{ .PublishableKey }}' + --header 'Authorization: Bearer ' */ From a459d7a02d65a72455131b7098cbc046663f1b60 Mon Sep 17 00:00:00 2001 From: Kalleby Santos Date: Fri, 10 Apr 2026 19:55:59 +0100 Subject: [PATCH 2/4] test: fix tests --- internal/functions/new/new_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/functions/new/new_test.go b/internal/functions/new/new_test.go index df082d0d05..3c395913f1 100644 --- a/internal/functions/new/new_test.go +++ b/internal/functions/new/new_test.go @@ -16,7 +16,7 @@ func TestNewCommand(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Run test - assert.NoError(t, Run(context.Background(), "test-func", fsys)) + assert.NoError(t, Run(context.Background(), "test-func", AuthAccessModeAlways, fsys)) // Validate output funcPath := filepath.Join(utils.FunctionsDir, "test-func", "index.ts") content, err := afero.ReadFile(fsys, funcPath) @@ -41,22 +41,22 @@ func TestNewCommand(t *testing.T) { }) t.Run("throws error on malformed slug", func(t *testing.T) { - assert.Error(t, Run(context.Background(), "@", afero.NewMemMapFs())) + assert.Error(t, Run(context.Background(), "@", AuthAccessModeAlways, afero.NewMemMapFs())) }) t.Run("throws error on duplicate slug", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() funcPath := filepath.Join(utils.FunctionsDir, "test-func", "index.ts") - require.NoError(t, afero.WriteFile(fsys, funcPath, []byte{}, 0644)) + require.NoError(t, afero.WriteFile(fsys, funcPath, []byte{}, 0o644)) // Run test - assert.Error(t, Run(context.Background(), "test-func", fsys)) + assert.Error(t, Run(context.Background(), "test-func", AuthAccessModeAlways, fsys)) }) t.Run("throws error on permission denied", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewReadOnlyFs(afero.NewMemMapFs()) // Run test - assert.Error(t, Run(context.Background(), "test-func", fsys)) + assert.Error(t, Run(context.Background(), "test-func", AuthAccessModeAlways, fsys)) }) } From 7c67467507dd9fb165fd0b8af62bcadaec709f17 Mon Sep 17 00:00:00 2001 From: Kalleby Santos Date: Fri, 10 Apr 2026 20:19:04 +0100 Subject: [PATCH 3/4] fix: wrong format type --- internal/functions/new/new.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/functions/new/new.go b/internal/functions/new/new.go index e6045a956d..6d1c189076 100644 --- a/internal/functions/new/new.go +++ b/internal/functions/new/new.go @@ -112,7 +112,7 @@ func createEntrypointFile(slug string, authMode AuthAccessMode, fsys afero.Fs) e defer f.Close() indexTemplate, hasTemplate := indexAuthTemplates[authMode] if !hasTemplate { - return errors.Errorf("failed to write entrypoint: '%w' is not a valid template", authMode) + return errors.Errorf("failed to write entrypoint: '%v' is not a valid template", authMode) } if err := indexTemplate.Option("missingkey=error").Execute(f, indexConfig{ URL: utils.GetApiUrl("/functions/v1/" + slug), From e5e44e95eed25953753b3149c70b60362e5fad93 Mon Sep 17 00:00:00 2001 From: Kalleby Santos Date: Fri, 10 Apr 2026 20:38:56 +0100 Subject: [PATCH 4/4] test: adding tests for auth templates --- internal/functions/new/new_test.go | 37 +++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/internal/functions/new/new_test.go b/internal/functions/new/new_test.go index 3c395913f1..22e4e72b49 100644 --- a/internal/functions/new/new_test.go +++ b/internal/functions/new/new_test.go @@ -2,6 +2,7 @@ package new import ( "context" + "fmt" "path/filepath" "testing" @@ -26,8 +27,11 @@ func TestNewCommand(t *testing.T) { ) // Verify config.toml is updated - _, err = afero.ReadFile(fsys, utils.ConfigPath) + content, err = afero.ReadFile(fsys, utils.ConfigPath) assert.NoError(t, err, "config.toml should be created") + assert.Contains(t, string(content), "[functions.test-func]") + // Always access mode should not verify jwt + assert.Contains(t, string(content), "verify_jwt = false") // Verify deno.json exists denoPath := filepath.Join(utils.FunctionsDir, "test-func", "deno.json") @@ -40,6 +44,37 @@ func TestNewCommand(t *testing.T) { assert.NoError(t, err, ".npmrc should be created") }) + t.Run("creates new function with apikey access", func(t *testing.T) { + fsys := afero.NewMemMapFs() + assert.NoError(t, Run(context.Background(), "test-func", AuthAccessModeApiKey, fsys)) + + // Validate output + funcPath := filepath.Join(utils.FunctionsDir, "test-func", "index.ts") + content, _ := afero.ReadFile(fsys, funcPath) + // Should contain the PublishableKey as example + assert.Contains(t, string(content), fmt.Sprintf("--header 'apiKey: %v'", utils.Config.Auth.PublishableKey.Value)) + + // Verify config.toml is updated to not verify jwt + content, _ = afero.ReadFile(fsys, utils.ConfigPath) + assert.Contains(t, string(content), "verify_jwt = false") + }) + + t.Run("creates new function with user access", func(t *testing.T) { + fsys := afero.NewMemMapFs() + assert.NoError(t, Run(context.Background(), "test-func", AuthAccessModeUser, fsys)) + + // Validate output + funcPath := filepath.Join(utils.FunctionsDir, "test-func", "index.ts") + content, _ := afero.ReadFile(fsys, funcPath) + // Should contain the PublishableKey as example as well placeholder for UserToken + assert.Contains(t, string(content), fmt.Sprintf("--header 'apiKey: %v'", utils.Config.Auth.PublishableKey.Value)) + assert.Contains(t, string(content), "--header 'Authorization: Bearer '") + + // Verify config.toml is updated and verify jwt enabled + content, _ = afero.ReadFile(fsys, utils.ConfigPath) + assert.Contains(t, string(content), "verify_jwt = true") + }) + t.Run("throws error on malformed slug", func(t *testing.T) { assert.Error(t, Run(context.Background(), "@", AuthAccessModeAlways, afero.NewMemMapFs())) })