From d2bc3ded8a72fca7ccaf69437a015e37b1a77b4c Mon Sep 17 00:00:00 2001 From: Alexander William Zlotnik Date: Sun, 1 Feb 2026 17:17:11 -0800 Subject: [PATCH 1/2] fix: restore session before auth middleware in SPA mode In SPA mode (useSsrCookies: false), there's a race condition where the auth-redirect middleware checks useSupabaseSession() before the session is hydrated from localStorage. This causes authenticated users to be incorrectly redirected to the login page on direct navigation or reload. This fix explicitly calls getSession() and populates the session state before the plugin setup completes, ensuring the session is available when middleware runs. Fixes #496 Co-Authored-By: Claude Opus 4.5 --- src/runtime/plugins/supabase.client.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/runtime/plugins/supabase.client.ts b/src/runtime/plugins/supabase.client.ts index 2319e86f..7a27ed40 100644 --- a/src/runtime/plugins/supabase.client.ts +++ b/src/runtime/plugins/supabase.client.ts @@ -44,6 +44,16 @@ export default defineNuxtPlugin({ const currentSession = useSupabaseSession() const currentUser = useSupabaseUser() + // In SPA mode, restore session from storage before auth middleware runs + // This prevents a race condition where middleware checks session before it's hydrated + // See: https://github.com/nuxt-modules/supabase/issues/496 + if (!useSsrCookies) { + const { data } = await client.auth.getSession() + if (data.session) { + currentSession.value = data.session + } + } + // Populate user before each page load to ensure the user state is correctly set before the page is rendered nuxtApp.hook('page:start', async () => { const { data } = await client.auth.getClaims() From ee3612d8e864d5c1e969edd48d50c8a56215d53d Mon Sep 17 00:00:00 2001 From: Alexander William Zlotnik Date: Sun, 1 Feb 2026 17:27:02 -0800 Subject: [PATCH 2/2] chore: add dist for git dependency usage --- dist/module.d.mts | 138 +++++++++++++++ dist/module.json | 12 ++ dist/module.mjs | 164 ++++++++++++++++++ .../composables/useSupabaseClient.d.ts | 3 + dist/runtime/composables/useSupabaseClient.js | 4 + .../useSupabaseCookieRedirect.d.ts | 13 ++ .../composables/useSupabaseCookieRedirect.js | 20 +++ .../composables/useSupabaseSession.d.ts | 7 + .../runtime/composables/useSupabaseSession.js | 2 + dist/runtime/composables/useSupabaseUser.d.ts | 6 + dist/runtime/composables/useSupabaseUser.js | 2 + dist/runtime/plugins/auth-redirect.d.ts | 3 + dist/runtime/plugins/auth-redirect.js | 40 +++++ dist/runtime/plugins/supabase.client.d.ts | 6 + dist/runtime/plugins/supabase.client.js | 62 +++++++ dist/runtime/plugins/supabase.server.d.ts | 6 + dist/runtime/plugins/supabase.server.js | 43 +++++ dist/runtime/server/services/index.d.ts | 4 + dist/runtime/server/services/index.js | 4 + .../server/services/serverSupabaseClient.d.ts | 4 + .../server/services/serverSupabaseClient.js | 26 +++ .../services/serverSupabaseServiceRole.d.ts | 4 + .../services/serverSupabaseServiceRole.js | 26 +++ .../services/serverSupabaseSession.d.ts | 3 + .../server/services/serverSupabaseSession.js | 11 ++ .../server/services/serverSupabaseUser.d.ts | 3 + .../server/services/serverSupabaseUser.js | 10 ++ dist/runtime/utils/cookies.d.ts | 7 + dist/runtime/utils/cookies.js | 14 ++ dist/runtime/utils/fetch-retry.d.ts | 1 + dist/runtime/utils/fetch-retry.js | 19 ++ dist/types.d.mts | 3 + 32 files changed, 670 insertions(+) create mode 100644 dist/module.d.mts create mode 100644 dist/module.json create mode 100644 dist/module.mjs create mode 100644 dist/runtime/composables/useSupabaseClient.d.ts create mode 100644 dist/runtime/composables/useSupabaseClient.js create mode 100644 dist/runtime/composables/useSupabaseCookieRedirect.d.ts create mode 100644 dist/runtime/composables/useSupabaseCookieRedirect.js create mode 100644 dist/runtime/composables/useSupabaseSession.d.ts create mode 100644 dist/runtime/composables/useSupabaseSession.js create mode 100644 dist/runtime/composables/useSupabaseUser.d.ts create mode 100644 dist/runtime/composables/useSupabaseUser.js create mode 100644 dist/runtime/plugins/auth-redirect.d.ts create mode 100644 dist/runtime/plugins/auth-redirect.js create mode 100644 dist/runtime/plugins/supabase.client.d.ts create mode 100644 dist/runtime/plugins/supabase.client.js create mode 100644 dist/runtime/plugins/supabase.server.d.ts create mode 100644 dist/runtime/plugins/supabase.server.js create mode 100644 dist/runtime/server/services/index.d.ts create mode 100644 dist/runtime/server/services/index.js create mode 100644 dist/runtime/server/services/serverSupabaseClient.d.ts create mode 100644 dist/runtime/server/services/serverSupabaseClient.js create mode 100644 dist/runtime/server/services/serverSupabaseServiceRole.d.ts create mode 100644 dist/runtime/server/services/serverSupabaseServiceRole.js create mode 100644 dist/runtime/server/services/serverSupabaseSession.d.ts create mode 100644 dist/runtime/server/services/serverSupabaseSession.js create mode 100644 dist/runtime/server/services/serverSupabaseUser.d.ts create mode 100644 dist/runtime/server/services/serverSupabaseUser.js create mode 100644 dist/runtime/utils/cookies.d.ts create mode 100644 dist/runtime/utils/cookies.js create mode 100644 dist/runtime/utils/fetch-retry.d.ts create mode 100644 dist/runtime/utils/fetch-retry.js create mode 100644 dist/types.d.mts diff --git a/dist/module.d.mts b/dist/module.d.mts new file mode 100644 index 00000000..b8d49222 --- /dev/null +++ b/dist/module.d.mts @@ -0,0 +1,138 @@ +import * as _nuxt_schema from '@nuxt/schema'; +import { CookieOptions } from 'nuxt/app'; +import { SupabaseClientOptions } from '@supabase/supabase-js'; + +declare module '@nuxt/schema' { + interface PublicRuntimeConfig { + supabase: { + url: string; + key: string; + redirect: boolean; + redirectOptions: RedirectOptions; + cookieName: string; + cookiePrefix: string; + useSsrCookies: boolean; + cookieOptions: CookieOptions; + types: string | false; + clientOptions: SupabaseClientOptions; + }; + } +} +interface RedirectOptions { + login: string; + callback: string; + include?: string[]; + exclude?: string[]; + /** + * @deprecated Use `saveRedirectToCookie` instead. + */ + cookieRedirect?: boolean; + /** + * If true, when automatically redirected the redirect path will be saved to a cookie, allowing retrieval later with the `useSupabaseRedirect` composable. + * @default false + */ + saveRedirectToCookie?: boolean; +} + +interface ModuleOptions { + /** + * Supabase API URL + * @default process.env.SUPABASE_URL + * @example 'https://*.supabase.co' + * @type string + * @docs https://supabase.com/docs/reference/javascript/initializing#parameters + */ + url: string; + /** + * Supabase Client publishable API Key (previously known as 'anon key') + * @default process.env.SUPABASE_KEY + * @example '123456789' + * @type string + * @docs https://supabase.com/docs/reference/javascript/initializing#parameters + */ + key: string; + /** + * Supabase Legacy 'service_role' key (deprecated) + * @default process.env.SUPABASE_SERVICE_KEY + * @example '123456789' + * @type string + * @docs https://supabase.com/docs/reference/javascript/initializing#parameters + * @deprecated Use `secretKey` instead. Will be removed in a future version. + */ + serviceKey: string; + /** + * Supabase Secret key + * @default process.env.SUPABASE_SECRET_KEY + * @example '123456789' + * @type string + * @docs https://supabase.com/blog/jwt-signing-keys + */ + secretKey: string; + /** + * Redirect automatically to login page if user is not authenticated + * @default `true` + * @type boolean + */ + redirect?: boolean; + /** + * Redirection options, set routes for login and callback redirect + * @default + * { + login: '/login', + callback: '/confirm', + exclude: [], + } + * @type RedirectOptions + */ + redirectOptions?: RedirectOptions; + /** + * Cookie name used for storing the redirect path when using the `redirect` option, added in front of `-redirect-path` to form the full cookie name e.g. `sb-redirect-path` + * @default 'sb' + * @type string + * @deprecated Use `cookiePrefix` instead. + */ + cookieName?: string; + /** + * The prefix used for all supabase cookies, and the redirect cookie. + * @default The default storage key from the supabase-js client. + * @type string + */ + cookiePrefix?: string; + /** + * If true, the supabase client will use cookies to store the session, allowing the session to be used from the server in ssr mode. + * Some `clientOptions` are not configurable when this is enabled. See the docs for more details. + * + * If false, the server will not be able to access the session. + * @default true + * @type boolean + */ + useSsrCookies?: boolean; + /** + * Cookie options + * @default { + maxAge: 60 * 60 * 8, + sameSite: 'lax', + secure: true, + } + * @type CookieOptions + * @docs https://nuxt.com/docs/api/composables/use-cookie#options + */ + cookieOptions?: CookieOptions; + /** + * Path to Supabase database type definitions file + * @default '~/types/database.types.ts' + * @type string + */ + types?: string | false; + /** + * Supabase client options (overrides default options from `@supabase/ssr`) + * @default { } + * @type object + * @docs https://supabase.com/docs/reference/javascript/initializing#parameters + */ + clientOptions?: SupabaseClientOptions; +} +declare const _default: _nuxt_schema.NuxtModule; + +export { _default as default }; +export type { ModuleOptions, RedirectOptions }; diff --git a/dist/module.json b/dist/module.json new file mode 100644 index 00000000..09758f8a --- /dev/null +++ b/dist/module.json @@ -0,0 +1,12 @@ +{ + "name": "@nuxtjs/supabase", + "configKey": "supabase", + "compatibility": { + "nuxt": ">=3.0.0" + }, + "version": "2.0.3", + "builder": { + "@nuxt/module-builder": "1.0.2", + "unbuild": "3.6.0" + } +} \ No newline at end of file diff --git a/dist/module.mjs b/dist/module.mjs new file mode 100644 index 00000000..48d48161 --- /dev/null +++ b/dist/module.mjs @@ -0,0 +1,164 @@ +import { fileURLToPath } from 'node:url'; +import fs from 'node:fs'; +import { defu } from 'defu'; +import { relative } from 'pathe'; +import { defineNuxtModule, useLogger, createResolver, addPlugin, addImportsDir, addTemplate, extendViteConfig } from '@nuxt/kit'; + +const module = defineNuxtModule({ + meta: { + name: "@nuxtjs/supabase", + configKey: "supabase", + compatibility: { + nuxt: ">=3.0.0" + } + }, + defaults: { + url: process.env.SUPABASE_URL, + key: process.env.SUPABASE_KEY, + serviceKey: process.env.SUPABASE_SERVICE_KEY, + secretKey: process.env.SUPABASE_SECRET_KEY, + redirect: true, + redirectOptions: { + login: "/login", + callback: "/confirm", + exclude: [], + cookieRedirect: false, + saveRedirectToCookie: false + }, + cookieName: "sb", + cookiePrefix: void 0, + useSsrCookies: true, + cookieOptions: { + maxAge: 60 * 60 * 8, + sameSite: "lax", + secure: true + }, + types: "~/types/database.types.ts", + clientOptions: {} + }, + setup(options, nuxt) { + const logger = useLogger("@nuxt/supabase"); + const { resolve, resolvePath } = createResolver(import.meta.url); + nuxt.options.runtimeConfig.public.supabase = defu(nuxt.options.runtimeConfig.public.supabase, { + url: options.url, + key: options.key, + redirect: options.redirect, + redirectOptions: options.redirectOptions, + cookieName: options.cookieName, + cookiePrefix: options.cookiePrefix, + useSsrCookies: options.useSsrCookies, + cookieOptions: options.cookieOptions, + clientOptions: options.clientOptions + }); + nuxt.options.runtimeConfig.supabase = defu(nuxt.options.runtimeConfig.supabase || {}, { + serviceKey: options.serviceKey, + secretKey: options.secretKey + }); + const finalUrl = nuxt.options.runtimeConfig.public.supabase.url; + if (!finalUrl) { + logger.warn("Missing supabase url, set it either in `nuxt.config.ts` or via env variable"); + } else { + try { + const defaultStorageKey = `sb-${new URL(finalUrl).hostname.split(".")[0]}-auth-token`; + const currentPrefix = nuxt.options.runtimeConfig.public.supabase.cookiePrefix; + nuxt.options.runtimeConfig.public.supabase.cookiePrefix = currentPrefix || defaultStorageKey; + } catch (error) { + logger.error( + `Invalid Supabase URL: "${finalUrl}". Please provide a valid URL (e.g., https://example.supabase.co or http://localhost:5432)`, + error + ); + const currentPrefix = nuxt.options.runtimeConfig.public.supabase.cookiePrefix; + nuxt.options.runtimeConfig.public.supabase.cookiePrefix = currentPrefix || "sb-auth-token"; + if (!nuxt.options.dev) { + throw new Error("Invalid Supabase URL configuration"); + } + } + } + if (!nuxt.options.runtimeConfig.public.supabase.key) { + logger.warn("Missing supabase publishable key, set it either in `nuxt.config.ts` or via env variable"); + } + if (nuxt.options.runtimeConfig.public.supabase.redirectOptions.cookieRedirect) { + logger.warn("The `cookieRedirect` option is deprecated, use `saveRedirectToCookie` instead."); + } + if (nuxt.options.runtimeConfig.public.supabase.cookieName != "sb") { + logger.warn("The `cookieName` option is deprecated, use `cookiePrefix` instead."); + } + const supabaseConfig = nuxt.options.runtimeConfig.supabase; + const hasServiceKey = !!supabaseConfig?.serviceKey; + const hasSecretKey = !!supabaseConfig?.secretKey; + if (hasServiceKey && !hasSecretKey) { + logger.warn("`SUPABASE_SERVICE_KEY` is deprecated and will be removed in a future version. Please migrate to `SUPABASE_SECRET_KEY` (JWT signing key). See: https://supabase.com/blog/jwt-signing-keys"); + } + const mergedOptions = nuxt.options.runtimeConfig.public.supabase; + if (mergedOptions.redirect && mergedOptions.redirectOptions.callback) { + const routeRules = {}; + routeRules[mergedOptions.redirectOptions.callback] = { ssr: false }; + nuxt.options.nitro = defu(nuxt.options.nitro, { + routeRules + }); + } + const runtimeDir = fileURLToPath(new URL("./runtime", import.meta.url)); + nuxt.options.build.transpile.push(runtimeDir); + addPlugin(resolve(runtimeDir, "plugins", "supabase.client")); + addPlugin(resolve(runtimeDir, "plugins", "supabase.server")); + if (mergedOptions.redirect) { + addPlugin(resolve(runtimeDir, "plugins", "auth-redirect")); + } + addImportsDir(resolve("./runtime/composables")); + nuxt.hook("nitro:config", (nitroConfig) => { + nitroConfig.alias = nitroConfig.alias || {}; + nitroConfig.externals = defu(typeof nitroConfig.externals === "object" ? nitroConfig.externals : {}, { + inline: [resolve("./runtime"), "@supabase/supabase-js"] + }); + nitroConfig.alias["#supabase/server"] = resolve("./runtime/server/services"); + nitroConfig.alias["#supabase/database"] = resolve(nitroConfig.buildDir, "types/supabase-database"); + }); + addTemplate({ + filename: "types/supabase.d.ts", + getContents: () => [ + "declare module '#supabase/server' {", + ` const serverSupabaseClient: typeof import('${resolve("./runtime/server/services")}').serverSupabaseClient`, + ` const serverSupabaseServiceRole: typeof import('${resolve( + "./runtime/server/services" + )}').serverSupabaseServiceRole`, + ` const serverSupabaseUser: typeof import('${resolve("./runtime/server/services")}').serverSupabaseUser`, + ` const serverSupabaseSession: typeof import('${resolve("./runtime/server/services")}').serverSupabaseSession`, + "}" + ].join("\n") + }); + addTemplate({ + filename: "types/supabase-database.d.ts", + getContents: async () => { + if (options.types) { + try { + const path = await resolvePath(options.types); + const typesPath = await resolvePath("~~/.nuxt/types/"); + if (fs.existsSync(path)) { + return `export * from '${relative(typesPath, path)}'`; + } else { + logger.warn( + `Database types configured at "${options.types}" but file not found at "${path}". Using "Database = unknown".` + ); + } + } catch (error) { + logger.error(`Failed to load Supabase database types from "${options.types}":`, error); + } + } + return `export type Database = unknown`; + } + }); + nuxt.hook("prepare:types", async (options2) => { + options2.references.push({ path: resolve(nuxt.options.buildDir, "types/supabase.d.ts") }); + }); + if (!nuxt.options.dev && !["cloudflare"].includes(process.env.NITRO_PRESET)) { + nuxt.options.build.transpile.push("websocket"); + } + extendViteConfig((config) => { + config.optimizeDeps = config.optimizeDeps || {}; + config.optimizeDeps.include = config.optimizeDeps.include || []; + config.optimizeDeps.include.push("@nuxtjs/supabase > cookie", "@nuxtjs/supabase > @supabase/postgrest-js", "@supabase/supabase-js"); + }); + } +}); + +export { module as default }; diff --git a/dist/runtime/composables/useSupabaseClient.d.ts b/dist/runtime/composables/useSupabaseClient.d.ts new file mode 100644 index 00000000..58de617c --- /dev/null +++ b/dist/runtime/composables/useSupabaseClient.d.ts @@ -0,0 +1,3 @@ +import type { SupabaseClient } from '@supabase/supabase-js'; +import type { Database } from '#build/types/supabase-database'; +export declare const useSupabaseClient: () => SupabaseClient; diff --git a/dist/runtime/composables/useSupabaseClient.js b/dist/runtime/composables/useSupabaseClient.js new file mode 100644 index 00000000..bcbd16b4 --- /dev/null +++ b/dist/runtime/composables/useSupabaseClient.js @@ -0,0 +1,4 @@ +import { useNuxtApp } from "#imports"; +export const useSupabaseClient = () => { + return useNuxtApp().$supabase.client; +}; diff --git a/dist/runtime/composables/useSupabaseCookieRedirect.d.ts b/dist/runtime/composables/useSupabaseCookieRedirect.d.ts new file mode 100644 index 00000000..9c891e3d --- /dev/null +++ b/dist/runtime/composables/useSupabaseCookieRedirect.d.ts @@ -0,0 +1,13 @@ +import type { CookieRef } from 'nuxt/app'; +export interface UseSupabaseCookieRedirectReturn { + /** + * The reactive value of the redirect path cookie. + * Can be both read and written to. + */ + path: CookieRef; + /** + * Get the current redirect path cookie value, then clear it + */ + pluck: () => string | null; +} +export declare const useSupabaseCookieRedirect: () => UseSupabaseCookieRedirectReturn; diff --git a/dist/runtime/composables/useSupabaseCookieRedirect.js b/dist/runtime/composables/useSupabaseCookieRedirect.js new file mode 100644 index 00000000..492011f8 --- /dev/null +++ b/dist/runtime/composables/useSupabaseCookieRedirect.js @@ -0,0 +1,20 @@ +import { useRuntimeConfig, useCookie } from "#imports"; +export const useSupabaseCookieRedirect = () => { + const config = useRuntimeConfig().public.supabase; + const prefix = config.redirectOptions.saveRedirectToCookie ? config.cookiePrefix : config.cookieName; + const cookie = useCookie( + `${prefix}-redirect-path`, + { + ...config.cookieOptions, + readonly: false + } + ); + return { + path: cookie, + pluck: () => { + const value = cookie.value; + cookie.value = null; + return value; + } + }; +}; diff --git a/dist/runtime/composables/useSupabaseSession.d.ts b/dist/runtime/composables/useSupabaseSession.d.ts new file mode 100644 index 00000000..4e6c6880 --- /dev/null +++ b/dist/runtime/composables/useSupabaseSession.d.ts @@ -0,0 +1,7 @@ +import type { Session } from '@supabase/supabase-js'; +import { type Ref } from '#imports'; +/** + * Reactive `Session` state from Supabase. This is initialized in both client and server plugin + * and, on the client, also updated through `onAuthStateChange` events. + */ +export declare const useSupabaseSession: () => Ref | null>; diff --git a/dist/runtime/composables/useSupabaseSession.js b/dist/runtime/composables/useSupabaseSession.js new file mode 100644 index 00000000..3f0059f8 --- /dev/null +++ b/dist/runtime/composables/useSupabaseSession.js @@ -0,0 +1,2 @@ +import { useState } from "#imports"; +export const useSupabaseSession = () => useState("supabase_session", () => null); diff --git a/dist/runtime/composables/useSupabaseUser.d.ts b/dist/runtime/composables/useSupabaseUser.d.ts new file mode 100644 index 00000000..54f123df --- /dev/null +++ b/dist/runtime/composables/useSupabaseUser.d.ts @@ -0,0 +1,6 @@ +import type { JwtPayload } from '@supabase/supabase-js'; +import { type Ref } from '#imports'; +/** + * Reactive `User` state from Supabase. This is populated by the JWT Payload from the auth.getClaims() call. + */ +export declare const useSupabaseUser: () => Ref; diff --git a/dist/runtime/composables/useSupabaseUser.js b/dist/runtime/composables/useSupabaseUser.js new file mode 100644 index 00000000..ed0c5589 --- /dev/null +++ b/dist/runtime/composables/useSupabaseUser.js @@ -0,0 +1,2 @@ +import { useState } from "#imports"; +export const useSupabaseUser = () => useState("supabase_user", () => null); diff --git a/dist/runtime/plugins/auth-redirect.d.ts b/dist/runtime/plugins/auth-redirect.d.ts new file mode 100644 index 00000000..2abe9597 --- /dev/null +++ b/dist/runtime/plugins/auth-redirect.d.ts @@ -0,0 +1,3 @@ +import type { Plugin } from '#app'; +declare const _default: Plugin; +export default _default; diff --git a/dist/runtime/plugins/auth-redirect.js b/dist/runtime/plugins/auth-redirect.js new file mode 100644 index 00000000..1582b523 --- /dev/null +++ b/dist/runtime/plugins/auth-redirect.js @@ -0,0 +1,40 @@ +import { useSupabaseCookieRedirect } from "../composables/useSupabaseCookieRedirect.js"; +import { useSupabaseSession } from "../composables/useSupabaseSession.js"; +import { defineNuxtPlugin, addRouteMiddleware, defineNuxtRouteMiddleware, useRuntimeConfig, navigateTo } from "#imports"; +function matchesAnyPattern(path, patterns) { + return patterns.some((pattern) => { + if (!pattern) return false; + const regex = new RegExp(`^${pattern.replace(/\*/g, ".*")}$`); + return regex.test(path); + }); +} +export default defineNuxtPlugin({ + name: "auth-redirect", + setup() { + addRouteMiddleware( + "global-auth", + defineNuxtRouteMiddleware((to) => { + const config = useRuntimeConfig().public.supabase; + const { login, callback, include, exclude, cookieRedirect, saveRedirectToCookie } = config.redirectOptions; + if (include && include.length > 0) { + if (!matchesAnyPattern(to.path, include)) { + return; + } + } + const excludePatterns = [login, callback, ...exclude ?? []]; + if (matchesAnyPattern(to.path, excludePatterns)) { + return; + } + const session = useSupabaseSession(); + if (!session.value) { + if (cookieRedirect || saveRedirectToCookie) { + const redirectInfo = useSupabaseCookieRedirect(); + redirectInfo.path.value = to.fullPath; + } + return navigateTo(login); + } + }), + { global: true } + ); + } +}); diff --git a/dist/runtime/plugins/supabase.client.d.ts b/dist/runtime/plugins/supabase.client.d.ts new file mode 100644 index 00000000..22665694 --- /dev/null +++ b/dist/runtime/plugins/supabase.client.d.ts @@ -0,0 +1,6 @@ +import { type SupabaseClient } from '@supabase/supabase-js'; +import type { Plugin } from '#app'; +declare const _default: Plugin<{ + client: SupabaseClient; +}>; +export default _default; diff --git a/dist/runtime/plugins/supabase.client.js b/dist/runtime/plugins/supabase.client.js new file mode 100644 index 00000000..82fc88aa --- /dev/null +++ b/dist/runtime/plugins/supabase.client.js @@ -0,0 +1,62 @@ +import { createBrowserClient } from "@supabase/ssr"; +import { createClient } from "@supabase/supabase-js"; +import { fetchWithRetry } from "../utils/fetch-retry.js"; +import { useSupabaseSession } from "../composables/useSupabaseSession.js"; +import { useSupabaseUser } from "../composables/useSupabaseUser.js"; +import { defineNuxtPlugin, useRuntimeConfig, useNuxtApp } from "#imports"; +export default defineNuxtPlugin({ + name: "supabase", + enforce: "pre", + async setup({ provide }) { + const nuxtApp = useNuxtApp(); + const { url, key, cookieOptions, cookiePrefix, useSsrCookies, clientOptions } = useRuntimeConfig().public.supabase; + let client; + if (useSsrCookies) { + client = createBrowserClient(url, key, { + ...clientOptions, + cookieOptions: { + ...cookieOptions, + name: cookiePrefix + }, + isSingleton: true, + global: { + fetch: fetchWithRetry, + ...clientOptions.global + } + }); + } else { + client = createClient(url, key, { + ...clientOptions, + global: { + fetch: fetchWithRetry, + ...clientOptions.global + } + }); + } + provide("supabase", { client }); + const currentSession = useSupabaseSession(); + const currentUser = useSupabaseUser(); + if (!useSsrCookies) { + const { data } = await client.auth.getSession(); + if (data.session) { + currentSession.value = data.session; + } + } + nuxtApp.hook("page:start", async () => { + const { data } = await client.auth.getClaims(); + currentUser.value = data?.claims ?? null; + }); + client.auth.onAuthStateChange((_, session) => { + if (JSON.stringify(currentSession.value) !== JSON.stringify(session)) { + currentSession.value = session; + if (session?.user) { + client.auth.getClaims().then(({ data }) => { + currentUser.value = data?.claims ?? null; + }); + } else { + currentUser.value = null; + } + } + }); + } +}); diff --git a/dist/runtime/plugins/supabase.server.d.ts b/dist/runtime/plugins/supabase.server.d.ts new file mode 100644 index 00000000..466a8381 --- /dev/null +++ b/dist/runtime/plugins/supabase.server.d.ts @@ -0,0 +1,6 @@ +import type { SupabaseClient } from '@supabase/supabase-js'; +import type { Plugin } from '#app'; +declare const _default: Plugin<{ + client: SupabaseClient; +}>; +export default _default; diff --git a/dist/runtime/plugins/supabase.server.js b/dist/runtime/plugins/supabase.server.js new file mode 100644 index 00000000..5c6b7e55 --- /dev/null +++ b/dist/runtime/plugins/supabase.server.js @@ -0,0 +1,43 @@ +import { createServerClient, parseCookieHeader } from "@supabase/ssr"; +import { getHeader } from "h3"; +import { fetchWithRetry } from "../utils/fetch-retry.js"; +import { setCookies } from "../utils/cookies.js"; +import { serverSupabaseUser, serverSupabaseSession } from "../server/services/index.js"; +import { useSupabaseSession } from "../composables/useSupabaseSession.js"; +import { useSupabaseUser } from "../composables/useSupabaseUser.js"; +import { defineNuxtPlugin, useRequestEvent, useRuntimeConfig } from "#imports"; +export default defineNuxtPlugin({ + name: "supabase", + enforce: "pre", + async setup({ provide }) { + const { url, key, cookiePrefix, useSsrCookies, cookieOptions, clientOptions } = useRuntimeConfig().public.supabase; + const event = useRequestEvent(); + const client = createServerClient(url, key, { + ...clientOptions, + cookies: { + getAll: () => parseCookieHeader(getHeader(event, "Cookie") ?? ""), + setAll: (cookies) => setCookies(event, cookies) + }, + cookieOptions: { + ...cookieOptions, + name: cookiePrefix + }, + global: { + fetch: fetchWithRetry, + ...clientOptions.global + } + }); + provide("supabase", { client }); + if (useSsrCookies) { + const [ + session, + user + ] = await Promise.all([ + serverSupabaseSession(event).catch(() => null), + serverSupabaseUser(event).catch(() => null) + ]); + useSupabaseSession().value = session; + useSupabaseUser().value = user; + } + } +}); diff --git a/dist/runtime/server/services/index.d.ts b/dist/runtime/server/services/index.d.ts new file mode 100644 index 00000000..7a86aebf --- /dev/null +++ b/dist/runtime/server/services/index.d.ts @@ -0,0 +1,4 @@ +export { serverSupabaseClient } from './serverSupabaseClient.js'; +export { serverSupabaseServiceRole } from './serverSupabaseServiceRole.js'; +export { serverSupabaseUser } from './serverSupabaseUser.js'; +export { serverSupabaseSession } from './serverSupabaseSession.js'; diff --git a/dist/runtime/server/services/index.js b/dist/runtime/server/services/index.js new file mode 100644 index 00000000..30b076a5 --- /dev/null +++ b/dist/runtime/server/services/index.js @@ -0,0 +1,4 @@ +export { serverSupabaseClient } from "./serverSupabaseClient.js"; +export { serverSupabaseServiceRole } from "./serverSupabaseServiceRole.js"; +export { serverSupabaseUser } from "./serverSupabaseUser.js"; +export { serverSupabaseSession } from "./serverSupabaseSession.js"; diff --git a/dist/runtime/server/services/serverSupabaseClient.d.ts b/dist/runtime/server/services/serverSupabaseClient.d.ts new file mode 100644 index 00000000..e5e683e0 --- /dev/null +++ b/dist/runtime/server/services/serverSupabaseClient.d.ts @@ -0,0 +1,4 @@ +import type { SupabaseClient } from '@supabase/supabase-js'; +import { type H3Event } from 'h3'; +import type { Database } from '#supabase/database'; +export declare const serverSupabaseClient: (event: H3Event) => Promise>; diff --git a/dist/runtime/server/services/serverSupabaseClient.js b/dist/runtime/server/services/serverSupabaseClient.js new file mode 100644 index 00000000..d397b8aa --- /dev/null +++ b/dist/runtime/server/services/serverSupabaseClient.js @@ -0,0 +1,26 @@ +import { createServerClient, parseCookieHeader } from "@supabase/ssr"; +import { getHeader } from "h3"; +import { fetchWithRetry } from "../../utils/fetch-retry.js"; +import { setCookies } from "../../utils/cookies.js"; +import { useRuntimeConfig } from "#imports"; +export const serverSupabaseClient = async (event) => { + if (!event.context._supabaseClient) { + const { url, key, cookiePrefix, cookieOptions, clientOptions: { auth = {}, global = {} } } = useRuntimeConfig(event).public.supabase; + event.context._supabaseClient = createServerClient(url, key, { + auth, + cookies: { + getAll: () => parseCookieHeader(getHeader(event, "Cookie") ?? ""), + setAll: (cookies) => setCookies(event, cookies) + }, + cookieOptions: { + ...cookieOptions, + name: cookiePrefix + }, + global: { + fetch: fetchWithRetry, + ...global + } + }); + } + return event.context._supabaseClient; +}; diff --git a/dist/runtime/server/services/serverSupabaseServiceRole.d.ts b/dist/runtime/server/services/serverSupabaseServiceRole.d.ts new file mode 100644 index 00000000..d99da535 --- /dev/null +++ b/dist/runtime/server/services/serverSupabaseServiceRole.d.ts @@ -0,0 +1,4 @@ +import type { SupabaseClient } from '@supabase/supabase-js'; +import type { H3Event } from 'h3'; +import type { Database } from '#supabase/database'; +export declare const serverSupabaseServiceRole: (event: H3Event) => SupabaseClient; diff --git a/dist/runtime/server/services/serverSupabaseServiceRole.js b/dist/runtime/server/services/serverSupabaseServiceRole.js new file mode 100644 index 00000000..99e6f6bb --- /dev/null +++ b/dist/runtime/server/services/serverSupabaseServiceRole.js @@ -0,0 +1,26 @@ +import { createClient } from "@supabase/supabase-js"; +import { fetchWithRetry } from "../../utils/fetch-retry.js"; +import { useRuntimeConfig } from "#imports"; +export const serverSupabaseServiceRole = (event) => { + const config = useRuntimeConfig(event); + const secretKey = config.supabase.secretKey; + const serviceKey = config.supabase.serviceKey; + const url = config.public.supabase.url; + const serverKey = secretKey || serviceKey; + if (!serverKey) { + throw new Error("Missing server key. Set either `SUPABASE_SECRET_KEY` (recommended) or `SUPABASE_SERVICE_KEY` (deprecated) in your environment variables."); + } + if (!event.context._supabaseServiceRole) { + event.context._supabaseServiceRole = createClient(url, serverKey, { + auth: { + detectSessionInUrl: false, + persistSession: false, + autoRefreshToken: false + }, + global: { + fetch: fetchWithRetry + } + }); + } + return event.context._supabaseServiceRole; +}; diff --git a/dist/runtime/server/services/serverSupabaseSession.d.ts b/dist/runtime/server/services/serverSupabaseSession.d.ts new file mode 100644 index 00000000..44d6ebc7 --- /dev/null +++ b/dist/runtime/server/services/serverSupabaseSession.d.ts @@ -0,0 +1,3 @@ +import type { Session } from '@supabase/supabase-js'; +import type { H3Event } from 'h3'; +export declare const serverSupabaseSession: (event: H3Event) => Promise | null>; diff --git a/dist/runtime/server/services/serverSupabaseSession.js b/dist/runtime/server/services/serverSupabaseSession.js new file mode 100644 index 00000000..e6077b41 --- /dev/null +++ b/dist/runtime/server/services/serverSupabaseSession.js @@ -0,0 +1,11 @@ +import { createError } from "h3"; +import { serverSupabaseClient } from "../services/serverSupabaseClient.js"; +export const serverSupabaseSession = async (event) => { + const client = await serverSupabaseClient(event); + const { data: { session }, error } = await client.auth.getSession(); + if (error) { + throw createError({ statusMessage: error?.message }); + } + delete session?.user; + return session; +}; diff --git a/dist/runtime/server/services/serverSupabaseUser.d.ts b/dist/runtime/server/services/serverSupabaseUser.d.ts new file mode 100644 index 00000000..a1e284ee --- /dev/null +++ b/dist/runtime/server/services/serverSupabaseUser.d.ts @@ -0,0 +1,3 @@ +import type { JwtPayload } from '@supabase/supabase-js'; +import type { H3Event } from 'h3'; +export declare const serverSupabaseUser: (event: H3Event) => Promise; diff --git a/dist/runtime/server/services/serverSupabaseUser.js b/dist/runtime/server/services/serverSupabaseUser.js new file mode 100644 index 00000000..1a4921b1 --- /dev/null +++ b/dist/runtime/server/services/serverSupabaseUser.js @@ -0,0 +1,10 @@ +import { createError } from "h3"; +import { serverSupabaseClient } from "../services/serverSupabaseClient.js"; +export const serverSupabaseUser = async (event) => { + const client = await serverSupabaseClient(event); + const { data, error } = await client.auth.getClaims(); + if (error) { + throw createError({ statusMessage: error?.message }); + } + return data?.claims ?? null; +}; diff --git a/dist/runtime/utils/cookies.d.ts b/dist/runtime/utils/cookies.d.ts new file mode 100644 index 00000000..5af73b10 --- /dev/null +++ b/dist/runtime/utils/cookies.d.ts @@ -0,0 +1,7 @@ +import { type H3Event } from 'h3'; +import type { CookieOptions } from '#app'; +export declare function setCookies(event: H3Event, cookies: { + name: string; + value: string; + options: CookieOptions; +}[]): void; diff --git a/dist/runtime/utils/cookies.js b/dist/runtime/utils/cookies.js new file mode 100644 index 00000000..c17a752c --- /dev/null +++ b/dist/runtime/utils/cookies.js @@ -0,0 +1,14 @@ +import { setCookie } from "h3"; +export function setCookies(event, cookies) { + const response = event.node.res; + const headersWritable = () => !response.headersSent && !response.writableEnded; + if (!headersWritable()) { + return; + } + for (const { name, value, options } of cookies) { + if (!headersWritable()) { + break; + } + setCookie(event, name, value, options); + } +} diff --git a/dist/runtime/utils/fetch-retry.d.ts b/dist/runtime/utils/fetch-retry.d.ts new file mode 100644 index 00000000..e8ace42e --- /dev/null +++ b/dist/runtime/utils/fetch-retry.d.ts @@ -0,0 +1 @@ +export declare function fetchWithRetry(req: RequestInfo | URL, init?: RequestInit): Promise; diff --git a/dist/runtime/utils/fetch-retry.js b/dist/runtime/utils/fetch-retry.js new file mode 100644 index 00000000..ee9794fc --- /dev/null +++ b/dist/runtime/utils/fetch-retry.js @@ -0,0 +1,19 @@ +export async function fetchWithRetry(req, init) { + const retries = 3; + for (let attempt = 1; attempt <= retries; attempt++) { + try { + return await fetch(req, init); + } catch (error) { + if (init?.signal?.aborted) { + throw error; + } + if (attempt === retries) { + console.error(`Error fetching request ${req}`, error, init); + throw error; + } + console.warn(`Retrying fetch attempt ${attempt + 1} for request: ${req}`); + await new Promise((resolve) => setTimeout(resolve, 100 * attempt)); + } + } + throw new Error("Unreachable code"); +} diff --git a/dist/types.d.mts b/dist/types.d.mts new file mode 100644 index 00000000..08f6bc8e --- /dev/null +++ b/dist/types.d.mts @@ -0,0 +1,3 @@ +export { default } from './module.mjs' + +export { type ModuleOptions, type RedirectOptions } from './module.mjs'