next-supa-utils 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +145 -3
- package/dist/client/index.cjs +187 -32
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +190 -1
- package/dist/client/index.d.ts +190 -1
- package/dist/client/index.js +177 -26
- package/dist/client/index.js.map +1 -1
- package/dist/server/index.cjs +92 -7
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +97 -2
- package/dist/server/index.d.ts +97 -2
- package/dist/server/index.js +91 -7
- package/dist/server/index.js.map +1 -1
- package/dist/shared/index.d.cts +10 -0
- package/dist/shared/index.d.ts +10 -0
- package/package.json +1 -1
package/dist/client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/client/hooks/useSupaUser.ts","../../src/shared/utils/error-handler.ts","../../src/client/hooks/useSupaSession.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\n\nimport type { UseSupaUserReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase user and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaUser } from \"next-supa-utils/client\";\n *\n * export default function Avatar() {\n * const { user, loading, error } = useSupaUser();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!user) return <p>Not signed in</p>;\n *\n * return <p>Hello, {user.email}</p>;\n * }\n * ```\n */\nexport function useSupaUser(): UseSupaUserReturn {\n const [state, setState] = useState<UseSupaUserReturn>({\n user: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n setState({\n user: null,\n loading: false,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n });\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getUser().then(({ data, error }) => {\n setState({\n user: data.user,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session) => {\n setState((prev) => ({\n ...prev,\n user: session?.user ?? null,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\nimport type { Session } from \"@supabase/supabase-js\";\n\nimport type { UseSupaSessionReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase session and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaSession } from \"next-supa-utils/client\";\n *\n * export default function TokenDisplay() {\n * const { session, loading, error } = useSupaSession();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!session) return <p>No active session</p>;\n *\n * return <p>Token expires at: {session.expires_at}</p>;\n * }\n * ```\n */\nexport function useSupaSession(): UseSupaSessionReturn {\n const [state, setState] = useState<UseSupaSessionReturn>({\n session: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n setState({\n session: null,\n loading: false,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n });\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getSession().then(({ data, error }) => {\n setState({\n session: data.session,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session: Session | null) => {\n setState((prev) => ({\n ...prev,\n session,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n"],"mappings":"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","names":["useEffect","useState","createBrowserClient","useState","useEffect","createBrowserClient"]}
|
|
1
|
+
{"version":3,"sources":["../../src/client/SupaProvider.tsx","../../src/client/hooks/useSupaUser.ts","../../src/shared/utils/error-handler.ts","../../src/client/hooks/useSupaSession.ts","../../src/client/hooks/useSupaUpload.ts","../../src/client/hooks/useSupaRealtime.ts"],"sourcesContent":["\"use client\";\n\nimport React, { createContext, useContext, ReactNode } from \"react\";\n\nexport interface SupaContextValue {\n /**\n * Explicit Supabase Project URL.\n * If omitted, the context falls back to `process.env.NEXT_PUBLIC_SUPABASE_URL`.\n */\n supabaseUrl?: string;\n\n /**\n * Explicit Supabase Anon Key.\n * If omitted, the context falls back to `process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY`.\n */\n supabaseAnonKey?: string;\n}\n\nconst SupaContext = createContext<SupaContextValue | undefined>(undefined);\n\n/**\n * `<SupaProvider>` allows you to inject explicit Supabase credentials\n * (URL and Anon Key) into all `next-supa-utils/client` hooks.\n *\n * It is completely **optional** if you are using the standard environment\n * variables (`NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY`).\n *\n * @example\n * ```tsx\n * // app/layout.tsx\n * import { SupaProvider } from \"next-supa-utils/client\";\n *\n * export default function RootLayout({ children }) {\n * return (\n * <html>\n * <body>\n * <SupaProvider supabaseUrl=\"https://...\" supabaseAnonKey=\"ey...\">\n * {children}\n * </SupaProvider>\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport function SupaProvider({\n children,\n supabaseUrl,\n supabaseAnonKey,\n}: { children: ReactNode } & SupaContextValue) {\n return (\n <SupaContext.Provider value={{ supabaseUrl, supabaseAnonKey }}>\n {children}\n </SupaContext.Provider>\n );\n}\n\n/**\n * Internal hook to retrieve the Supabase configuration from Context or Environment Variables.\n */\nexport function useSupaConfig() {\n const context = useContext(SupaContext);\n\n const url = context?.supabaseUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL;\n const key = context?.supabaseAnonKey ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!url || !key) {\n throw new Error(\n \"[next-supa-utils] Missing Supabase configuration. Provide NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables, or wrap your tree in <SupaProvider supabaseUrl={...} supabaseAnonKey={...}>.\",\n );\n }\n\n return { url, key };\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\n\nimport type { UseSupaUserReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\nimport { useSupaConfig } from \"../SupaProvider\";\n\n/**\n * React hook that provides the current Supabase user and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaUser } from \"next-supa-utils/client\";\n *\n * export default function Avatar() {\n * const { user, loading, error } = useSupaUser();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!user) return <p>Not signed in</p>;\n *\n * return <p>Hello, {user.email}</p>;\n * }\n * ```\n */\nexport function useSupaUser(): UseSupaUserReturn {\n const [state, setState] = useState<UseSupaUserReturn>({\n user: null,\n loading: true,\n error: null,\n });\n\n // Get config from Context or Environment\n const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();\n\n useEffect(() => {\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getUser().then(({ data, error }) => {\n setState({\n user: data.user,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session) => {\n setState((prev) => ({\n ...prev,\n user: session?.user ?? null,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\nimport type { Session } from \"@supabase/supabase-js\";\n\nimport type { UseSupaSessionReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\nimport { useSupaConfig } from \"../SupaProvider\";\n\n/**\n * React hook that provides the current Supabase session and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaSession } from \"next-supa-utils/client\";\n *\n * export default function TokenDisplay() {\n * const { session, loading, error } = useSupaSession();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!session) return <p>No active session</p>;\n *\n * return <p>Token expires at: {session.expires_at}</p>;\n * }\n * ```\n */\nexport function useSupaSession(): UseSupaSessionReturn {\n const [state, setState] = useState<UseSupaSessionReturn>({\n session: null,\n loading: true,\n error: null,\n });\n\n // Get config from Context or Environment\n const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();\n\n useEffect(() => {\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getSession().then(({ data, error }) => {\n setState({\n session: data.session,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session: Session | null) => {\n setState((prev) => ({\n ...prev,\n session,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n","\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\n\nimport type { UseSupaUploadReturn, UploadOptions } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\nimport { useSupaConfig } from \"../SupaProvider\";\n\n/**\n * React hook that simplifies uploading files to Supabase Storage\n * with **real-time progress tracking**.\n *\n * Uses `XMLHttpRequest` against the Supabase Storage REST API so that\n * `progress` updates smoothly from 0 → 100 as bytes are sent, without\n * requiring `tus-js-client` or any extra dependencies.\n *\n * @param bucketName - The name of the Supabase Storage bucket to upload to.\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaUpload } from \"next-supa-utils/client\";\n *\n * export default function AvatarUploader() {\n * const { upload, isUploading, progress, data, error } = useSupaUpload(\"avatars\");\n *\n * const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {\n * const file = e.target.files?.[0];\n * if (!file) return;\n * await upload(file, { path: `users/${file.name}`, upsert: true });\n * };\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={handleChange} disabled={isUploading} />\n * {isUploading && <progress value={progress} max={100} />}\n * {isUploading && <p>{progress}%</p>}\n * {error && <p>Error: {error.message}</p>}\n * {data && <p>Uploaded to: {data.path}</p>}\n * </div>\n * );\n * }\n * ```\n */\nexport function useSupaUpload(bucketName: string): UseSupaUploadReturn {\n const [isUploading, setIsUploading] = useState(false);\n const [progress, setProgress] = useState(0);\n const [data, setData] = useState<UseSupaUploadReturn[\"data\"]>(null);\n const [error, setError] = useState<UseSupaUploadReturn[\"error\"]>(null);\n\n // Abort controller reference so we can cancel in-flight uploads.\n const xhrRef = useRef<XMLHttpRequest | null>(null);\n\n // Stable reference to the Supabase client (used only to get the session token).\n const supabaseRef = useRef<ReturnType<typeof createBrowserClient> | null>(null);\n\n // Get config from Context or Environment\n const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();\n\n function getClient() {\n if (supabaseRef.current) return supabaseRef.current;\n supabaseRef.current = createBrowserClient(supabaseUrl, supabaseAnonKey);\n return supabaseRef.current;\n }\n\n const upload = useCallback(\n async (file: File, options?: UploadOptions) => {\n // ── Reset state ──────────────────────────────────────────────\n setIsUploading(true);\n setProgress(0);\n setData(null);\n setError(null);\n\n try {\n const supabase = getClient();\n\n // Retrieve the current session token for authorization.\n const {\n data: { session },\n } = await supabase.auth.getSession();\n\n const accessToken = session?.access_token ?? supabaseAnonKey;\n const filePath = options?.path ?? file.name;\n\n // ── Build the Storage REST API URL ─────────────────────────\n // POST /storage/v1/object/:bucket/:path\n const uploadUrl = `${supabaseUrl}/storage/v1/object/${bucketName}/${filePath}`;\n\n // ── Upload via XHR for real-time progress ──────────────────\n await new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhrRef.current = xhr;\n\n // ── Progress handler ───────────────────────────────────\n xhr.upload.addEventListener(\"progress\", (e) => {\n if (e.lengthComputable) {\n const pct = Math.round((e.loaded / e.total) * 100);\n setProgress(pct);\n }\n });\n\n // ── Success handler ────────────────────────────────────\n xhr.addEventListener(\"load\", () => {\n xhrRef.current = null;\n\n if (xhr.status >= 200 && xhr.status < 300) {\n try {\n const response = JSON.parse(xhr.responseText) as {\n Key?: string;\n Id?: string;\n };\n // Supabase returns { Key: \"bucket/path\" }\n const fullPath = response.Key ?? `${bucketName}/${filePath}`;\n setData({ path: filePath, fullPath });\n setProgress(100);\n resolve();\n } catch {\n // Response parsed fine even if body structure differs\n setData({ path: filePath, fullPath: `${bucketName}/${filePath}` });\n setProgress(100);\n resolve();\n }\n } else {\n // Server returned an error status\n try {\n const errBody = JSON.parse(xhr.responseText) as {\n statusCode?: string;\n error?: string;\n message?: string;\n };\n reject(\n new Error(\n errBody.message ?? errBody.error ?? `Upload failed with status ${xhr.status}`,\n ),\n );\n } catch {\n reject(new Error(`Upload failed with status ${xhr.status}`));\n }\n }\n });\n\n // ── Error handler ──────────────────────────────────────\n xhr.addEventListener(\"error\", () => {\n xhrRef.current = null;\n reject(new Error(\"Network error during upload\"));\n });\n\n // ── Abort handler ──────────────────────────────────────\n xhr.addEventListener(\"abort\", () => {\n xhrRef.current = null;\n reject(new Error(\"Upload cancelled\"));\n });\n\n // ── Send the request ───────────────────────────────────\n xhr.open(\"POST\", uploadUrl, true);\n xhr.setRequestHeader(\"Authorization\", `Bearer ${accessToken}`);\n xhr.setRequestHeader(\"apikey\", supabaseAnonKey);\n xhr.setRequestHeader(\n \"Content-Type\",\n options?.contentType ?? (file.type || \"application/octet-stream\"),\n );\n xhr.setRequestHeader(\n \"cache-control\",\n options?.cacheControl ?? \"3600\",\n );\n xhr.setRequestHeader(\n \"x-upsert\",\n String(options?.upsert ?? false),\n );\n\n xhr.send(file);\n });\n } catch (caught: unknown) {\n setError(handleSupaError(caught));\n setProgress(0);\n } finally {\n setIsUploading(false);\n }\n },\n [bucketName],\n );\n\n const cancel = useCallback(() => {\n if (xhrRef.current) {\n xhrRef.current.abort();\n xhrRef.current = null;\n }\n }, []);\n\n const reset = useCallback(() => {\n cancel();\n setIsUploading(false);\n setProgress(0);\n setData(null);\n setError(null);\n }, [cancel]);\n\n return { upload, isUploading, progress, data, error, reset, cancel };\n}\n","\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\nimport type { RealtimeChannel } from \"@supabase/supabase-js\";\n\nimport type { RealtimeEvent, RealtimePayload } from \"../../types\";\nimport { useSupaConfig } from \"../SupaProvider\";\n\n/**\n * React hook that subscribes to Supabase Realtime postgres_changes\n * events and **safely cleans up** on unmount to prevent memory leaks.\n *\n * @param table - The database table to listen to.\n * @param event - The event type: `\"INSERT\"`, `\"UPDATE\"`, `\"DELETE\"`, or `\"*\"` for all.\n * @param callback - Function called with the realtime payload on each event.\n * @param schema - The database schema (defaults to `\"public\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaRealtime } from \"next-supa-utils/client\";\n *\n * export default function LiveMessages() {\n * const [messages, setMessages] = useState<Message[]>([]);\n *\n * useSupaRealtime(\"messages\", \"INSERT\", (payload) => {\n * setMessages((prev) => [...prev, payload.new as Message]);\n * });\n *\n * return <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Listen to all events with a custom schema\n * useSupaRealtime(\"orders\", \"*\", (payload) => {\n * console.log(payload.eventType, payload.new, payload.old);\n * }, \"inventory\");\n * ```\n */\nexport function useSupaRealtime<T extends Record<string, unknown> = Record<string, unknown>>(\n table: string,\n event: RealtimeEvent,\n callback: (payload: RealtimePayload<T>) => void,\n schema: string = \"public\",\n): void {\n // Store callback in a ref so the channel doesn't need to re-subscribe\n // when only the callback identity changes (common with inline arrows).\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n // Store the channel so we can clean it up.\n const channelRef = useRef<RealtimeChannel | null>(null);\n\n // Get config from Context or Environment\n const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();\n\n useEffect(() => {\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // Generate a unique channel name to avoid collisions.\n const channelName = `realtime:${schema}:${table}:${event}:${Date.now()}`;\n\n const channel = supabase\n .channel(channelName)\n .on(\n \"postgres_changes\" as \"postgres_changes\",\n {\n event: event === \"*\" ? \"*\" : event,\n schema,\n table,\n },\n (payload) => {\n callbackRef.current(payload as unknown as RealtimePayload<T>);\n },\n )\n .subscribe();\n\n channelRef.current = channel;\n\n // ── Cleanup: remove the channel on unmount or dep change ──────\n return () => {\n if (channelRef.current) {\n supabase.removeChannel(channelRef.current);\n channelRef.current = null;\n }\n };\n }, [table, event, schema]); // Re-subscribe when table/event/schema changes\n}\n"],"mappings":"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","names":["useEffect","useState","createBrowserClient","useState","useEffect","createBrowserClient","useState","createBrowserClient","useState","createBrowserClient","useEffect","useRef","createBrowserClient","useRef","useEffect","createBrowserClient"]}
|
package/dist/server/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var server_exports = {};
|
|
22
22
|
__export(server_exports, {
|
|
23
23
|
createAction: () => createAction,
|
|
24
|
+
routeWrapper: () => routeWrapper,
|
|
24
25
|
withSupaAuth: () => withSupaAuth
|
|
25
26
|
});
|
|
26
27
|
module.exports = __toCommonJS(server_exports);
|
|
@@ -67,8 +68,8 @@ function withSupaAuth(options) {
|
|
|
67
68
|
let response = import_server.NextResponse.next({
|
|
68
69
|
request: { headers: request.headers }
|
|
69
70
|
});
|
|
70
|
-
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
71
|
-
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
71
|
+
const supabaseUrl = options.supabaseUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
72
|
+
const supabaseAnonKey = options.supabaseAnonKey ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
72
73
|
if (!supabaseUrl || !supabaseAnonKey) {
|
|
73
74
|
console.error(
|
|
74
75
|
"[next-supa-utils] Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables."
|
|
@@ -151,11 +152,11 @@ function handleSupaError(error) {
|
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
// src/server/actions/actionWrapper.ts
|
|
154
|
-
function createAction(fn) {
|
|
155
|
+
function createAction(fn, options) {
|
|
155
156
|
return async (...args) => {
|
|
156
157
|
try {
|
|
157
|
-
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
158
|
-
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
158
|
+
const supabaseUrl = options?.supabaseUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
159
|
+
const supabaseAnonKey = options?.supabaseAnonKey ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
159
160
|
if (!supabaseUrl || !supabaseAnonKey) {
|
|
160
161
|
return {
|
|
161
162
|
data: null,
|
|
@@ -173,8 +174,8 @@ function createAction(fn) {
|
|
|
173
174
|
},
|
|
174
175
|
setAll(cookiesToSet) {
|
|
175
176
|
try {
|
|
176
|
-
cookiesToSet.forEach(({ name, value, options }) => {
|
|
177
|
-
cookieStore.set(name, value,
|
|
177
|
+
cookiesToSet.forEach(({ name, value, options: options2 }) => {
|
|
178
|
+
cookieStore.set(name, value, options2);
|
|
178
179
|
});
|
|
179
180
|
} catch {
|
|
180
181
|
}
|
|
@@ -189,9 +190,93 @@ function createAction(fn) {
|
|
|
189
190
|
}
|
|
190
191
|
};
|
|
191
192
|
}
|
|
193
|
+
|
|
194
|
+
// src/server/actions/routeWrapper.ts
|
|
195
|
+
var import_ssr3 = require("@supabase/ssr");
|
|
196
|
+
var import_server2 = require("next/server");
|
|
197
|
+
var import_headers2 = require("next/headers");
|
|
198
|
+
function routeWrapper(handler, options) {
|
|
199
|
+
const { requireAuth = false, supabaseUrl: optUrl, supabaseAnonKey: optKey } = options ?? {};
|
|
200
|
+
return async (request, routeContext) => {
|
|
201
|
+
try {
|
|
202
|
+
const supabaseUrl = optUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
203
|
+
const supabaseAnonKey = optKey ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
204
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
205
|
+
return import_server2.NextResponse.json(
|
|
206
|
+
{ error: { message: "Server configuration error", code: "CONFIG_ERROR" } },
|
|
207
|
+
{ status: 500 }
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const ctx = {
|
|
211
|
+
params: routeContext?.params ? await routeContext.params : {},
|
|
212
|
+
supabase: null,
|
|
213
|
+
user: null
|
|
214
|
+
};
|
|
215
|
+
if (requireAuth) {
|
|
216
|
+
const cookieStore = await (0, import_headers2.cookies)();
|
|
217
|
+
const supabase = (0, import_ssr3.createServerClient)(supabaseUrl, supabaseAnonKey, {
|
|
218
|
+
cookies: {
|
|
219
|
+
getAll() {
|
|
220
|
+
return cookieStore.getAll();
|
|
221
|
+
},
|
|
222
|
+
setAll(cookiesToSet) {
|
|
223
|
+
try {
|
|
224
|
+
cookiesToSet.forEach(({ name, value, options: opts }) => {
|
|
225
|
+
cookieStore.set(name, value, opts);
|
|
226
|
+
});
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
const {
|
|
233
|
+
data: { user },
|
|
234
|
+
error: authError
|
|
235
|
+
} = await supabase.auth.getUser();
|
|
236
|
+
if (authError || !user) {
|
|
237
|
+
return import_server2.NextResponse.json(
|
|
238
|
+
{
|
|
239
|
+
error: {
|
|
240
|
+
message: "Unauthorized: valid session required",
|
|
241
|
+
code: "UNAUTHORIZED"
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
{ status: 401 }
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
ctx.supabase = supabase;
|
|
248
|
+
ctx.user = user;
|
|
249
|
+
} else {
|
|
250
|
+
const cookieStore = await (0, import_headers2.cookies)();
|
|
251
|
+
ctx.supabase = (0, import_ssr3.createServerClient)(supabaseUrl, supabaseAnonKey, {
|
|
252
|
+
cookies: {
|
|
253
|
+
getAll() {
|
|
254
|
+
return cookieStore.getAll();
|
|
255
|
+
},
|
|
256
|
+
setAll(cookiesToSet) {
|
|
257
|
+
try {
|
|
258
|
+
cookiesToSet.forEach(({ name, value, options: opts }) => {
|
|
259
|
+
cookieStore.set(name, value, opts);
|
|
260
|
+
});
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const response = await handler(request, ctx);
|
|
268
|
+
return response instanceof import_server2.NextResponse ? response : import_server2.NextResponse.json(await response.json(), { status: response.status });
|
|
269
|
+
} catch (caught) {
|
|
270
|
+
const error = handleSupaError(caught);
|
|
271
|
+
const status = error.status ?? 500;
|
|
272
|
+
return import_server2.NextResponse.json({ error }, { status });
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
192
276
|
// Annotate the CommonJS export names for ESM import in node:
|
|
193
277
|
0 && (module.exports = {
|
|
194
278
|
createAction,
|
|
279
|
+
routeWrapper,
|
|
195
280
|
withSupaAuth
|
|
196
281
|
});
|
|
197
282
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/index.ts","../../src/server/middleware/withSupaAuth.ts","../../src/server/actions/actionWrapper.ts","../../src/shared/utils/error-handler.ts"],"sourcesContent":["// ── Server entry point ──────────────────────────────────────────────\n// This module should ONLY be imported in server-side contexts:\n// middleware.ts, Server Components, Server Actions, Route Handlers.\n\nexport { withSupaAuth } from \"./middleware/withSupaAuth\";\nexport { createAction } from \"./actions/actionWrapper\";\n\n// Re-export types consumers commonly need alongside server helpers.\nexport type { MiddlewareOptions, RouteConfig, ActionResponse, SupaError } from \"../types\";\n","import { createServerClient, type CookieOptions } from \"@supabase/ssr\";\nimport { NextResponse, type NextRequest } from \"next/server\";\n\nimport type { MiddlewareOptions, RouteConfig } from \"../../types\";\n\n// ── Path matching utility ───────────────────────────────────────────\n\n/**\n * Converts a route pattern into a RegExp for matching.\n *\n * Supports:\n * - Exact: `\"/settings\"` → matches only `/settings`\n * - Prefix: `\"/dashboard\"` → matches `/dashboard`, `/dashboard/stats`\n * - Wildcard: `\"/admin/:path*\"` → matches `/admin`, `/admin/users`, `/admin/a/b/c`\n */\nfunction matchPath(pattern: string, pathname: string): boolean {\n // Wildcard pattern: \"/admin/:path*\" → match \"/admin\" and everything below\n if (pattern.includes(\":path*\")) {\n const prefix = pattern.replace(/:path\\*$/, \"\").replace(/\\/$/, \"\");\n return pathname === prefix || pathname.startsWith(prefix + \"/\");\n }\n\n // Prefix matching: \"/dashboard\" matches \"/dashboard/anything\"\n return pathname === pattern || pathname.startsWith(pattern + \"/\");\n}\n\n/**\n * Finds the first `RouteConfig` whose pattern matches `pathname`, or `undefined`.\n */\nfunction findMatchingRoute(\n routes: RouteConfig[],\n pathname: string,\n): RouteConfig | undefined {\n return routes.find((route) => matchPath(route.path, pathname));\n}\n\n// ── Role extraction utility ─────────────────────────────────────────\n\ntype UserMeta = {\n user_metadata: Record<string, unknown>;\n app_metadata: Record<string, unknown>;\n};\n\nfunction extractRole(\n user: UserMeta,\n extractor: MiddlewareOptions[\"roleExtractor\"],\n): string | string[] | undefined {\n if (typeof extractor === \"function\") {\n return extractor(user);\n }\n\n const source =\n extractor === \"app_metadata\" ? user.app_metadata : user.user_metadata;\n\n const raw = source?.role;\n\n if (typeof raw === \"string\") return raw;\n if (Array.isArray(raw) && raw.every((r) => typeof r === \"string\")) {\n return raw as string[];\n }\n\n return undefined;\n}\n\n/**\n * Check whether the user's role(s) satisfy the route's `allowedRoles`.\n */\nfunction hasRequiredRole(\n userRole: string | string[] | undefined,\n allowedRoles: string[] | undefined,\n): boolean {\n // No role restriction → any authenticated user is allowed.\n if (!allowedRoles || allowedRoles.length === 0) return true;\n\n if (!userRole) return false;\n\n const roles = Array.isArray(userRole) ? userRole : [userRole];\n return roles.some((r) => allowedRoles.includes(r));\n}\n\n// ── Main middleware factory ─────────────────────────────────────────\n\n/**\n * Create a Next.js middleware handler that protects routes with\n * authentication **and** optional role-based access control (RBAC).\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { withSupaAuth } from \"next-supa-utils/server\";\n *\n * export default withSupaAuth({\n * routes: [\n * { path: \"/dashboard\" }, // any authed user\n * { path: \"/admin/:path*\", allowedRoles: [\"admin\"] }, // admin only\n * { path: \"/editor/:path*\", allowedRoles: [\"admin\", \"editor\"] },\n * ],\n * redirectTo: \"/login\",\n * roleExtractor: \"user_metadata\", // or \"app_metadata\" or a custom fn\n * });\n *\n * export const config = {\n * matcher: [\"/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)\"],\n * };\n * ```\n */\nexport function withSupaAuth(options: MiddlewareOptions) {\n const {\n routes,\n redirectTo = \"/login\",\n roleExtractor = \"user_metadata\",\n onAuthSuccess,\n } = options;\n\n return async function middleware(request: NextRequest): Promise<NextResponse> {\n // ── 1. Create a mutable response so Supabase can set cookies ──\n let response = NextResponse.next({\n request: { headers: request.headers },\n });\n\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n console.error(\n \"[next-supa-utils] Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n );\n return response;\n }\n\n // ── 2. Initialize server client with middleware cookie helpers ─\n const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return request.cookies.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n cookiesToSet.forEach(({ name, value }) => {\n request.cookies.set(name, value);\n });\n\n response = NextResponse.next({ request });\n\n cookiesToSet.forEach(({ name, value, options }) => {\n response.cookies.set(name, value, options);\n });\n },\n },\n });\n\n // ── 3. Refresh session (required to keep tokens alive) ────────\n const {\n data: { user },\n } = await supabase.auth.getUser();\n\n const { pathname } = request.nextUrl;\n\n // ── 4. Find the matching route config ─────────────────────────\n const matchedRoute = findMatchingRoute(routes, pathname);\n\n // No matching route → not a protected path, pass through.\n if (!matchedRoute) {\n return response;\n }\n\n // ── 5. Check authentication ───────────────────────────────────\n if (!user) {\n const loginUrl = new URL(redirectTo, request.url);\n loginUrl.searchParams.set(\"next\", pathname);\n return NextResponse.redirect(loginUrl);\n }\n\n // ── 6. Check role-based access ────────────────────────────────\n const userRole = extractRole(user, roleExtractor);\n\n if (!hasRequiredRole(userRole, matchedRoute.allowedRoles)) {\n // User is logged in but lacks the required role.\n const forbiddenUrl = new URL(redirectTo, request.url);\n forbiddenUrl.searchParams.set(\"error\", \"forbidden\");\n forbiddenUrl.searchParams.set(\"next\", pathname);\n return NextResponse.redirect(forbiddenUrl);\n }\n\n // ── 7. Success callback ───────────────────────────────────────\n if (onAuthSuccess) {\n await onAuthSuccess({\n id: user.id,\n email: user.email ?? undefined,\n role: userRole,\n });\n }\n\n return response;\n };\n}\n","import { createServerClient, type CookieOptions } from \"@supabase/ssr\";\nimport { cookies } from \"next/headers\";\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\n\nimport type { ActionResponse, SupaError } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * Create a type-safe Server Action that automatically:\n * 1. Initialises a Supabase server client (with cookies)\n * 2. Wraps execution in try/catch\n * 3. Returns a standardised `{ data, error }` response\n *\n * @example\n * ```ts\n * // app/actions/profile.ts\n * \"use server\";\n * import { createAction } from \"next-supa-utils/server\";\n *\n * export const getProfile = createAction(async (supabase, userId: string) => {\n * const { data, error } = await supabase\n * .from(\"profiles\")\n * .select(\"*\")\n * .eq(\"id\", userId)\n * .single();\n *\n * if (error) throw error;\n * return data;\n * });\n *\n * // Usage in a Server Component or Client Component:\n * const result = await getProfile(\"user-uuid\");\n * if (result.error) { ... }\n * ```\n */\nexport function createAction<TArgs extends unknown[], TResult>(\n fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult>,\n): (...args: TArgs) => Promise<ActionResponse<TResult>> {\n return async (...args: TArgs): Promise<ActionResponse<TResult>> => {\n try {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n return {\n data: null,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n };\n }\n\n const cookieStore = await cookies();\n\n const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return cookieStore.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n try {\n cookiesToSet.forEach(({ name, value, options }) => {\n cookieStore.set(name, value, options);\n });\n } catch {\n // `cookies().set()` throws when called from a Server Component.\n // In that context we only need read access — the middleware\n // handles token refresh.\n }\n },\n },\n });\n\n const data = await fn(supabase, ...args);\n\n return { data, error: null };\n } catch (caught: unknown) {\n const error: SupaError = handleSupaError(caught);\n return { data: null, error };\n }\n };\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n"],"mappings":"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","names":["options","import_ssr"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/index.ts","../../src/server/middleware/withSupaAuth.ts","../../src/server/actions/actionWrapper.ts","../../src/shared/utils/error-handler.ts","../../src/server/actions/routeWrapper.ts"],"sourcesContent":["// ── Server entry point ──────────────────────────────────────────────\n// This module should ONLY be imported in server-side contexts:\n// middleware.ts, Server Components, Server Actions, Route Handlers.\n\nexport { withSupaAuth } from \"./middleware/withSupaAuth\";\nexport { createAction } from \"./actions/actionWrapper\";\nexport { routeWrapper } from \"./actions/routeWrapper\";\nexport type { RouteHandlerContext } from \"./actions/routeWrapper\";\n\n// Re-export types consumers commonly need alongside server helpers.\nexport type { MiddlewareOptions, RouteConfig, RouteWrapperOptions, ActionResponse, SupaError } from \"../types\";\n","import { createServerClient, type CookieOptions } from \"@supabase/ssr\";\nimport { NextResponse, type NextRequest } from \"next/server\";\n\nimport type { MiddlewareOptions, RouteConfig } from \"../../types\";\n\n// ── Path matching utility ───────────────────────────────────────────\n\n/**\n * Converts a route pattern into a RegExp for matching.\n *\n * Supports:\n * - Exact: `\"/settings\"` → matches only `/settings`\n * - Prefix: `\"/dashboard\"` → matches `/dashboard`, `/dashboard/stats`\n * - Wildcard: `\"/admin/:path*\"` → matches `/admin`, `/admin/users`, `/admin/a/b/c`\n */\nfunction matchPath(pattern: string, pathname: string): boolean {\n // Wildcard pattern: \"/admin/:path*\" → match \"/admin\" and everything below\n if (pattern.includes(\":path*\")) {\n const prefix = pattern.replace(/:path\\*$/, \"\").replace(/\\/$/, \"\");\n return pathname === prefix || pathname.startsWith(prefix + \"/\");\n }\n\n // Prefix matching: \"/dashboard\" matches \"/dashboard/anything\"\n return pathname === pattern || pathname.startsWith(pattern + \"/\");\n}\n\n/**\n * Finds the first `RouteConfig` whose pattern matches `pathname`, or `undefined`.\n */\nfunction findMatchingRoute(\n routes: RouteConfig[],\n pathname: string,\n): RouteConfig | undefined {\n return routes.find((route) => matchPath(route.path, pathname));\n}\n\n// ── Role extraction utility ─────────────────────────────────────────\n\ntype UserMeta = {\n user_metadata: Record<string, unknown>;\n app_metadata: Record<string, unknown>;\n};\n\nfunction extractRole(\n user: UserMeta,\n extractor: MiddlewareOptions[\"roleExtractor\"],\n): string | string[] | undefined {\n if (typeof extractor === \"function\") {\n return extractor(user);\n }\n\n const source =\n extractor === \"app_metadata\" ? user.app_metadata : user.user_metadata;\n\n const raw = source?.role;\n\n if (typeof raw === \"string\") return raw;\n if (Array.isArray(raw) && raw.every((r) => typeof r === \"string\")) {\n return raw as string[];\n }\n\n return undefined;\n}\n\n/**\n * Check whether the user's role(s) satisfy the route's `allowedRoles`.\n */\nfunction hasRequiredRole(\n userRole: string | string[] | undefined,\n allowedRoles: string[] | undefined,\n): boolean {\n // No role restriction → any authenticated user is allowed.\n if (!allowedRoles || allowedRoles.length === 0) return true;\n\n if (!userRole) return false;\n\n const roles = Array.isArray(userRole) ? userRole : [userRole];\n return roles.some((r) => allowedRoles.includes(r));\n}\n\n// ── Main middleware factory ─────────────────────────────────────────\n\n/**\n * Create a Next.js middleware handler that protects routes with\n * authentication **and** optional role-based access control (RBAC).\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { withSupaAuth } from \"next-supa-utils/server\";\n *\n * export default withSupaAuth({\n * routes: [\n * { path: \"/dashboard\" }, // any authed user\n * { path: \"/admin/:path*\", allowedRoles: [\"admin\"] }, // admin only\n * { path: \"/editor/:path*\", allowedRoles: [\"admin\", \"editor\"] },\n * ],\n * redirectTo: \"/login\",\n * roleExtractor: \"user_metadata\", // or \"app_metadata\" or a custom fn\n * });\n *\n * export const config = {\n * matcher: [\"/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)\"],\n * };\n * ```\n */\nexport function withSupaAuth(options: MiddlewareOptions) {\n const {\n routes,\n redirectTo = \"/login\",\n roleExtractor = \"user_metadata\",\n onAuthSuccess,\n } = options;\n\n return async function middleware(request: NextRequest): Promise<NextResponse> {\n // ── 1. Create a mutable response so Supabase can set cookies ──\n let response = NextResponse.next({\n request: { headers: request.headers },\n });\n\n const supabaseUrl = options.supabaseUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = options.supabaseAnonKey ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n console.error(\n \"[next-supa-utils] Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n );\n return response;\n }\n\n // ── 2. Initialize server client with middleware cookie helpers ─\n const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return request.cookies.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n cookiesToSet.forEach(({ name, value }) => {\n request.cookies.set(name, value);\n });\n\n response = NextResponse.next({ request });\n\n cookiesToSet.forEach(({ name, value, options }) => {\n response.cookies.set(name, value, options);\n });\n },\n },\n });\n\n // ── 3. Refresh session (required to keep tokens alive) ────────\n const {\n data: { user },\n } = await supabase.auth.getUser();\n\n const { pathname } = request.nextUrl;\n\n // ── 4. Find the matching route config ─────────────────────────\n const matchedRoute = findMatchingRoute(routes, pathname);\n\n // No matching route → not a protected path, pass through.\n if (!matchedRoute) {\n return response;\n }\n\n // ── 5. Check authentication ───────────────────────────────────\n if (!user) {\n const loginUrl = new URL(redirectTo, request.url);\n loginUrl.searchParams.set(\"next\", pathname);\n return NextResponse.redirect(loginUrl);\n }\n\n // ── 6. Check role-based access ────────────────────────────────\n const userRole = extractRole(user, roleExtractor);\n\n if (!hasRequiredRole(userRole, matchedRoute.allowedRoles)) {\n // User is logged in but lacks the required role.\n const forbiddenUrl = new URL(redirectTo, request.url);\n forbiddenUrl.searchParams.set(\"error\", \"forbidden\");\n forbiddenUrl.searchParams.set(\"next\", pathname);\n return NextResponse.redirect(forbiddenUrl);\n }\n\n // ── 7. Success callback ───────────────────────────────────────\n if (onAuthSuccess) {\n await onAuthSuccess({\n id: user.id,\n email: user.email ?? undefined,\n role: userRole,\n });\n }\n\n return response;\n };\n}\n","import { createServerClient, type CookieOptions } from \"@supabase/ssr\";\nimport { cookies } from \"next/headers\";\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\n\nimport type { ActionResponse, SupaError, ServerActionOptions } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * Create a type-safe Server Action that automatically:\n * 1. Initialises a Supabase server client (with cookies)\n * 2. Wraps execution in try/catch\n * 3. Returns a standardised `{ data, error }` response\n *\n * @example\n * ```ts\n * // app/actions/profile.ts\n * \"use server\";\n * import { createAction } from \"next-supa-utils/server\";\n *\n * export const getProfile = createAction(async (supabase, userId: string) => {\n * const { data, error } = await supabase\n * .from(\"profiles\")\n * .select(\"*\")\n * .eq(\"id\", userId)\n * .single();\n *\n * if (error) throw error;\n * return data;\n * });\n *\n * // Usage in a Server Component or Client Component:\n * const result = await getProfile(\"user-uuid\");\n * if (result.error) { ... }\n * ```\n */\nexport function createAction<TArgs extends unknown[], TResult>(\n fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult>,\n options?: ServerActionOptions,\n): (...args: TArgs) => Promise<ActionResponse<TResult>> {\n return async (...args: TArgs): Promise<ActionResponse<TResult>> => {\n try {\n const supabaseUrl = options?.supabaseUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = options?.supabaseAnonKey ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n return {\n data: null,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n };\n }\n\n const cookieStore = await cookies();\n\n const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return cookieStore.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n try {\n cookiesToSet.forEach(({ name, value, options }) => {\n cookieStore.set(name, value, options);\n });\n } catch {\n // `cookies().set()` throws when called from a Server Component.\n // In that context we only need read access — the middleware\n // handles token refresh.\n }\n },\n },\n });\n\n const data = await fn(supabase, ...args);\n\n return { data, error: null };\n } catch (caught: unknown) {\n const error: SupaError = handleSupaError(caught);\n return { data: null, error };\n }\n };\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n","import { createServerClient, type CookieOptions } from \"@supabase/ssr\";\nimport { NextResponse, type NextRequest } from \"next/server\";\nimport { cookies } from \"next/headers\";\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\n\nimport type { RouteWrapperOptions, SupaError } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * Higher-order function that wraps a Next.js App Router Route Handler\n * with standardized error handling and optional authentication gating.\n *\n * @example\n * ```ts\n * // app/api/posts/route.ts\n * import { routeWrapper } from \"next-supa-utils/server\";\n *\n * // Public endpoint (no auth required)\n * export const GET = routeWrapper(async (request) => {\n * const data = await fetchPosts();\n * return NextResponse.json({ data });\n * });\n *\n * // Protected endpoint (requires valid Supabase session)\n * export const POST = routeWrapper(\n * async (request, { supabase, user }) => {\n * const body = await request.json();\n * const { data, error } = await supabase\n * .from(\"posts\")\n * .insert({ ...body, user_id: user.id })\n * .select()\n * .single();\n *\n * if (error) throw error;\n * return NextResponse.json({ data }, { status: 201 });\n * },\n * { requireAuth: true },\n * );\n * ```\n */\nexport function routeWrapper<TContext extends Record<string, unknown> = Record<string, unknown>>(\n handler: (\n request: NextRequest,\n context: RouteHandlerContext<TContext>,\n ) => Promise<NextResponse | Response>,\n options?: RouteWrapperOptions,\n) {\n const { requireAuth = false, supabaseUrl: optUrl, supabaseAnonKey: optKey } = options ?? {};\n\n return async (\n request: NextRequest,\n routeContext?: { params?: Promise<TContext> },\n ): Promise<NextResponse> => {\n try {\n const supabaseUrl = optUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = optKey ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n return NextResponse.json(\n { error: { message: \"Server configuration error\", code: \"CONFIG_ERROR\" } },\n { status: 500 },\n );\n }\n\n // ── Build context to pass to the handler ────────────────────\n const ctx: RouteHandlerContext<TContext> = {\n params: routeContext?.params ? await routeContext.params : ({} as TContext),\n supabase: null as unknown as SupabaseClient,\n user: null,\n };\n\n // ── Optionally init Supabase & check auth ───────────────────\n if (requireAuth) {\n const cookieStore = await cookies();\n\n const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return cookieStore.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n try {\n cookiesToSet.forEach(({ name, value, options: opts }) => {\n cookieStore.set(name, value, opts);\n });\n } catch {\n // cookies().set() may throw in read-only contexts\n }\n },\n },\n });\n\n const {\n data: { user },\n error: authError,\n } = await supabase.auth.getUser();\n\n if (authError || !user) {\n return NextResponse.json(\n {\n error: {\n message: \"Unauthorized: valid session required\",\n code: \"UNAUTHORIZED\",\n } satisfies SupaError,\n },\n { status: 401 },\n );\n }\n\n ctx.supabase = supabase;\n ctx.user = user;\n } else {\n // Even for public routes, provide a Supabase client for convenience.\n const cookieStore = await cookies();\n\n ctx.supabase = createServerClient(supabaseUrl, supabaseAnonKey, {\n cookies: {\n getAll() {\n return cookieStore.getAll();\n },\n setAll(cookiesToSet: { name: string; value: string; options?: CookieOptions }[]) {\n try {\n cookiesToSet.forEach(({ name, value, options: opts }) => {\n cookieStore.set(name, value, opts);\n });\n } catch {\n // cookies().set() may throw in read-only contexts\n }\n },\n },\n });\n }\n\n // ── Execute the handler ─────────────────────────────────────\n const response = await handler(request, ctx);\n return response instanceof NextResponse\n ? response\n : NextResponse.json(await response.json(), { status: response.status });\n } catch (caught: unknown) {\n const error = handleSupaError(caught);\n const status = error.status ?? 500;\n\n return NextResponse.json({ error }, { status });\n }\n };\n}\n\n// ── Internal context type ───────────────────────────────────────────\n\n/** Context object passed to the route handler by `routeWrapper`. */\nexport interface RouteHandlerContext<\n TParams extends Record<string, unknown> = Record<string, unknown>,\n> {\n /** Resolved dynamic route params (e.g. `{ id: \"123\" }`). */\n params: TParams;\n\n /**\n * Supabase server client.\n * Always available (even on public routes) for convenience queries.\n */\n supabase: SupabaseClient;\n\n /**\n * The authenticated user, or `null` for public (non-auth) routes.\n * Guaranteed non-null when `requireAuth: true`.\n */\n user: Awaited<ReturnType<SupabaseClient[\"auth\"][\"getUser\"]>>[\"data\"][\"user\"];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,iBAAuD;AACvD,oBAA+C;AAc/C,SAAS,UAAU,SAAiB,UAA2B;AAE7D,MAAI,QAAQ,SAAS,QAAQ,GAAG;AAC9B,UAAM,SAAS,QAAQ,QAAQ,YAAY,EAAE,EAAE,QAAQ,OAAO,EAAE;AAChE,WAAO,aAAa,UAAU,SAAS,WAAW,SAAS,GAAG;AAAA,EAChE;AAGA,SAAO,aAAa,WAAW,SAAS,WAAW,UAAU,GAAG;AAClE;AAKA,SAAS,kBACP,QACA,UACyB;AACzB,SAAO,OAAO,KAAK,CAAC,UAAU,UAAU,MAAM,MAAM,QAAQ,CAAC;AAC/D;AASA,SAAS,YACP,MACA,WAC+B;AAC/B,MAAI,OAAO,cAAc,YAAY;AACnC,WAAO,UAAU,IAAI;AAAA,EACvB;AAEA,QAAM,SACJ,cAAc,iBAAiB,KAAK,eAAe,KAAK;AAE1D,QAAM,MAAM,QAAQ;AAEpB,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,MAAM,QAAQ,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ,GAAG;AACjE,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAKA,SAAS,gBACP,UACA,cACS;AAET,MAAI,CAAC,gBAAgB,aAAa,WAAW,EAAG,QAAO;AAEvD,MAAI,CAAC,SAAU,QAAO;AAEtB,QAAM,QAAQ,MAAM,QAAQ,QAAQ,IAAI,WAAW,CAAC,QAAQ;AAC5D,SAAO,MAAM,KAAK,CAAC,MAAM,aAAa,SAAS,CAAC,CAAC;AACnD;AA4BO,SAAS,aAAa,SAA4B;AACvD,QAAM;AAAA,IACJ;AAAA,IACA,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB;AAAA,EACF,IAAI;AAEJ,SAAO,eAAe,WAAW,SAA6C;AAE5E,QAAI,WAAW,2BAAa,KAAK;AAAA,MAC/B,SAAS,EAAE,SAAS,QAAQ,QAAQ;AAAA,IACtC,CAAC;AAED,UAAM,cAAc,QAAQ,eAAe,QAAQ,IAAI;AACvD,UAAM,kBAAkB,QAAQ,mBAAmB,QAAQ,IAAI;AAE/D,QAAI,CAAC,eAAe,CAAC,iBAAiB;AACpC,cAAQ;AAAA,QACN;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAGA,UAAM,eAAW,+BAAmB,aAAa,iBAAiB;AAAA,MAChE,SAAS;AAAA,QACP,SAAS;AACP,iBAAO,QAAQ,QAAQ,OAAO;AAAA,QAChC;AAAA,QACA,OAAO,cAA0E;AAC/E,uBAAa,QAAQ,CAAC,EAAE,MAAM,MAAM,MAAM;AACxC,oBAAQ,QAAQ,IAAI,MAAM,KAAK;AAAA,UACjC,CAAC;AAED,qBAAW,2BAAa,KAAK,EAAE,QAAQ,CAAC;AAExC,uBAAa,QAAQ,CAAC,EAAE,MAAM,OAAO,SAAAA,SAAQ,MAAM;AACjD,qBAAS,QAAQ,IAAI,MAAM,OAAOA,QAAO;AAAA,UAC3C,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF,CAAC;AAGD,UAAM;AAAA,MACJ,MAAM,EAAE,KAAK;AAAA,IACf,IAAI,MAAM,SAAS,KAAK,QAAQ;AAEhC,UAAM,EAAE,SAAS,IAAI,QAAQ;AAG7B,UAAM,eAAe,kBAAkB,QAAQ,QAAQ;AAGvD,QAAI,CAAC,cAAc;AACjB,aAAO;AAAA,IACT;AAGA,QAAI,CAAC,MAAM;AACT,YAAM,WAAW,IAAI,IAAI,YAAY,QAAQ,GAAG;AAChD,eAAS,aAAa,IAAI,QAAQ,QAAQ;AAC1C,aAAO,2BAAa,SAAS,QAAQ;AAAA,IACvC;AAGA,UAAM,WAAW,YAAY,MAAM,aAAa;AAEhD,QAAI,CAAC,gBAAgB,UAAU,aAAa,YAAY,GAAG;AAEzD,YAAM,eAAe,IAAI,IAAI,YAAY,QAAQ,GAAG;AACpD,mBAAa,aAAa,IAAI,SAAS,WAAW;AAClD,mBAAa,aAAa,IAAI,QAAQ,QAAQ;AAC9C,aAAO,2BAAa,SAAS,YAAY;AAAA,IAC3C;AAGA,QAAI,eAAe;AACjB,YAAM,cAAc;AAAA,QAClB,IAAI,KAAK;AAAA,QACT,OAAO,KAAK,SAAS;AAAA,QACrB,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AACF;;;AClMA,IAAAC,cAAuD;AACvD,qBAAwB;;;ACUjB,SAAS,gBAAgB,OAA2B;AAEzD,MAAI,iBAAiB,OAAO;AAC1B,UAAM,SAAS;AACf,WAAO;AAAA,MACL,SAAS,MAAM;AAAA,MACf,MAAM,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO;AAAA,MACtD,QAAQ,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AAAA,IAC9D;AAAA,EACF;AAGA,MACE,OAAO,UAAU,YACjB,UAAU,QACV,aAAa,SACb,OAAQ,MAAkC,YAAY,UACtD;AACA,UAAM,MAAM;AACZ,WAAO;AAAA,MACL,SAAS,IAAI;AAAA,MACb,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AAAA,MAChD,QAAQ,OAAO,IAAI,WAAW,WAAW,IAAI,SAAS;AAAA,IACxD;AAAA,EACF;AAGA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B;AAGA,SAAO,EAAE,SAAS,4BAA4B;AAChD;;;ADTO,SAAS,aACd,IACA,SACsD;AACtD,SAAO,UAAU,SAAkD;AACjE,QAAI;AACF,YAAM,cAAc,SAAS,eAAe,QAAQ,IAAI;AACxD,YAAM,kBAAkB,SAAS,mBAAmB,QAAQ,IAAI;AAEhE,UAAI,CAAC,eAAe,CAAC,iBAAiB;AACpC,eAAO;AAAA,UACL,MAAM;AAAA,UACN,OAAO;AAAA,YACL,SACE;AAAA,YACF,MAAM;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAEA,YAAM,cAAc,UAAM,wBAAQ;AAElC,YAAM,eAAW,gCAAmB,aAAa,iBAAiB;AAAA,QAChE,SAAS;AAAA,UACP,SAAS;AACP,mBAAO,YAAY,OAAO;AAAA,UAC5B;AAAA,UACA,OAAO,cAA0E;AAC/E,gBAAI;AACF,2BAAa,QAAQ,CAAC,EAAE,MAAM,OAAO,SAAAC,SAAQ,MAAM;AACjD,4BAAY,IAAI,MAAM,OAAOA,QAAO;AAAA,cACtC,CAAC;AAAA,YACH,QAAQ;AAAA,YAIR;AAAA,UACF;AAAA,QACF;AAAA,MACF,CAAC;AAED,YAAM,OAAO,MAAM,GAAG,UAAU,GAAG,IAAI;AAEvC,aAAO,EAAE,MAAM,OAAO,KAAK;AAAA,IAC7B,SAAS,QAAiB;AACxB,YAAM,QAAmB,gBAAgB,MAAM;AAC/C,aAAO,EAAE,MAAM,MAAM,MAAM;AAAA,IAC7B;AAAA,EACF;AACF;;;AEpFA,IAAAC,cAAuD;AACvD,IAAAC,iBAA+C;AAC/C,IAAAC,kBAAwB;AAsCjB,SAAS,aACd,SAIA,SACA;AACA,QAAM,EAAE,cAAc,OAAO,aAAa,QAAQ,iBAAiB,OAAO,IAAI,WAAW,CAAC;AAE1F,SAAO,OACL,SACA,iBAC0B;AAC1B,QAAI;AACF,YAAM,cAAc,UAAU,QAAQ,IAAI;AAC1C,YAAM,kBAAkB,UAAU,QAAQ,IAAI;AAE9C,UAAI,CAAC,eAAe,CAAC,iBAAiB;AACpC,eAAO,4BAAa;AAAA,UAClB,EAAE,OAAO,EAAE,SAAS,8BAA8B,MAAM,eAAe,EAAE;AAAA,UACzE,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAGA,YAAM,MAAqC;AAAA,QACzC,QAAQ,cAAc,SAAS,MAAM,aAAa,SAAU,CAAC;AAAA,QAC7D,UAAU;AAAA,QACV,MAAM;AAAA,MACR;AAGA,UAAI,aAAa;AACf,cAAM,cAAc,UAAM,yBAAQ;AAElC,cAAM,eAAW,gCAAmB,aAAa,iBAAiB;AAAA,UAChE,SAAS;AAAA,YACP,SAAS;AACP,qBAAO,YAAY,OAAO;AAAA,YAC5B;AAAA,YACA,OAAO,cAA0E;AAC/E,kBAAI;AACF,6BAAa,QAAQ,CAAC,EAAE,MAAM,OAAO,SAAS,KAAK,MAAM;AACvD,8BAAY,IAAI,MAAM,OAAO,IAAI;AAAA,gBACnC,CAAC;AAAA,cACH,QAAQ;AAAA,cAER;AAAA,YACF;AAAA,UACF;AAAA,QACF,CAAC;AAED,cAAM;AAAA,UACJ,MAAM,EAAE,KAAK;AAAA,UACb,OAAO;AAAA,QACT,IAAI,MAAM,SAAS,KAAK,QAAQ;AAEhC,YAAI,aAAa,CAAC,MAAM;AACtB,iBAAO,4BAAa;AAAA,YAClB;AAAA,cACE,OAAO;AAAA,gBACL,SAAS;AAAA,gBACT,MAAM;AAAA,cACR;AAAA,YACF;AAAA,YACA,EAAE,QAAQ,IAAI;AAAA,UAChB;AAAA,QACF;AAEA,YAAI,WAAW;AACf,YAAI,OAAO;AAAA,MACb,OAAO;AAEL,cAAM,cAAc,UAAM,yBAAQ;AAElC,YAAI,eAAW,gCAAmB,aAAa,iBAAiB;AAAA,UAC9D,SAAS;AAAA,YACP,SAAS;AACP,qBAAO,YAAY,OAAO;AAAA,YAC5B;AAAA,YACA,OAAO,cAA0E;AAC/E,kBAAI;AACF,6BAAa,QAAQ,CAAC,EAAE,MAAM,OAAO,SAAS,KAAK,MAAM;AACvD,8BAAY,IAAI,MAAM,OAAO,IAAI;AAAA,gBACnC,CAAC;AAAA,cACH,QAAQ;AAAA,cAER;AAAA,YACF;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH;AAGA,YAAM,WAAW,MAAM,QAAQ,SAAS,GAAG;AAC3C,aAAO,oBAAoB,8BACvB,WACA,4BAAa,KAAK,MAAM,SAAS,KAAK,GAAG,EAAE,QAAQ,SAAS,OAAO,CAAC;AAAA,IAC1E,SAAS,QAAiB;AACxB,YAAM,QAAQ,gBAAgB,MAAM;AACpC,YAAM,SAAS,MAAM,UAAU;AAE/B,aAAO,4BAAa,KAAK,EAAE,MAAM,GAAG,EAAE,OAAO,CAAC;AAAA,IAChD;AAAA,EACF;AACF;","names":["options","import_ssr","options","import_ssr","import_server","import_headers"]}
|
package/dist/server/index.d.cts
CHANGED
|
@@ -57,6 +57,16 @@ interface MiddlewareOptions {
|
|
|
57
57
|
* @default "/login"
|
|
58
58
|
*/
|
|
59
59
|
redirectTo?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Explicit Supabase Project URL.
|
|
62
|
+
* If omitted, falls back to `process.env.NEXT_PUBLIC_SUPABASE_URL`.
|
|
63
|
+
*/
|
|
64
|
+
supabaseUrl?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Explicit Supabase Anon Key.
|
|
67
|
+
* If omitted, falls back to `process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY`.
|
|
68
|
+
*/
|
|
69
|
+
supabaseAnonKey?: string;
|
|
60
70
|
/**
|
|
61
71
|
* Strategy for extracting the user's role from the Supabase user object.
|
|
62
72
|
*
|
|
@@ -80,6 +90,40 @@ interface MiddlewareOptions {
|
|
|
80
90
|
role?: string | string[];
|
|
81
91
|
}) => void | Promise<void>;
|
|
82
92
|
}
|
|
93
|
+
/** Options for the `routeWrapper` higher-order function. */
|
|
94
|
+
interface RouteWrapperOptions {
|
|
95
|
+
/**
|
|
96
|
+
* If `true`, the wrapper will initialise a Supabase server client,
|
|
97
|
+
* verify the session via `getUser()`, and reject with 401 if invalid.
|
|
98
|
+
* The `context.supabase` and `context.user` are guaranteed non-null.
|
|
99
|
+
*
|
|
100
|
+
* @default false
|
|
101
|
+
*/
|
|
102
|
+
requireAuth?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Explicit Supabase Project URL.
|
|
105
|
+
* If omitted, falls back to `process.env.NEXT_PUBLIC_SUPABASE_URL`.
|
|
106
|
+
*/
|
|
107
|
+
supabaseUrl?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Explicit Supabase Anon Key.
|
|
110
|
+
* If omitted, falls back to `process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY`.
|
|
111
|
+
*/
|
|
112
|
+
supabaseAnonKey?: string;
|
|
113
|
+
}
|
|
114
|
+
/** Options for the `createAction` wrapper. */
|
|
115
|
+
interface ServerActionOptions {
|
|
116
|
+
/**
|
|
117
|
+
* Explicit Supabase Project URL.
|
|
118
|
+
* If omitted, falls back to `process.env.NEXT_PUBLIC_SUPABASE_URL`.
|
|
119
|
+
*/
|
|
120
|
+
supabaseUrl?: string;
|
|
121
|
+
/**
|
|
122
|
+
* Explicit Supabase Anon Key.
|
|
123
|
+
* If omitted, falls back to `process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY`.
|
|
124
|
+
*/
|
|
125
|
+
supabaseAnonKey?: string;
|
|
126
|
+
}
|
|
83
127
|
|
|
84
128
|
/**
|
|
85
129
|
* Create a Next.js middleware handler that protects routes with
|
|
@@ -135,6 +179,57 @@ declare function withSupaAuth(options: MiddlewareOptions): (request: NextRequest
|
|
|
135
179
|
* if (result.error) { ... }
|
|
136
180
|
* ```
|
|
137
181
|
*/
|
|
138
|
-
declare function createAction<TArgs extends unknown[], TResult>(fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult
|
|
182
|
+
declare function createAction<TArgs extends unknown[], TResult>(fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult>, options?: ServerActionOptions): (...args: TArgs) => Promise<ActionResponse<TResult>>;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Higher-order function that wraps a Next.js App Router Route Handler
|
|
186
|
+
* with standardized error handling and optional authentication gating.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* // app/api/posts/route.ts
|
|
191
|
+
* import { routeWrapper } from "next-supa-utils/server";
|
|
192
|
+
*
|
|
193
|
+
* // Public endpoint (no auth required)
|
|
194
|
+
* export const GET = routeWrapper(async (request) => {
|
|
195
|
+
* const data = await fetchPosts();
|
|
196
|
+
* return NextResponse.json({ data });
|
|
197
|
+
* });
|
|
198
|
+
*
|
|
199
|
+
* // Protected endpoint (requires valid Supabase session)
|
|
200
|
+
* export const POST = routeWrapper(
|
|
201
|
+
* async (request, { supabase, user }) => {
|
|
202
|
+
* const body = await request.json();
|
|
203
|
+
* const { data, error } = await supabase
|
|
204
|
+
* .from("posts")
|
|
205
|
+
* .insert({ ...body, user_id: user.id })
|
|
206
|
+
* .select()
|
|
207
|
+
* .single();
|
|
208
|
+
*
|
|
209
|
+
* if (error) throw error;
|
|
210
|
+
* return NextResponse.json({ data }, { status: 201 });
|
|
211
|
+
* },
|
|
212
|
+
* { requireAuth: true },
|
|
213
|
+
* );
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
declare function routeWrapper<TContext extends Record<string, unknown> = Record<string, unknown>>(handler: (request: NextRequest, context: RouteHandlerContext<TContext>) => Promise<NextResponse | Response>, options?: RouteWrapperOptions): (request: NextRequest, routeContext?: {
|
|
217
|
+
params?: Promise<TContext>;
|
|
218
|
+
}) => Promise<NextResponse>;
|
|
219
|
+
/** Context object passed to the route handler by `routeWrapper`. */
|
|
220
|
+
interface RouteHandlerContext<TParams extends Record<string, unknown> = Record<string, unknown>> {
|
|
221
|
+
/** Resolved dynamic route params (e.g. `{ id: "123" }`). */
|
|
222
|
+
params: TParams;
|
|
223
|
+
/**
|
|
224
|
+
* Supabase server client.
|
|
225
|
+
* Always available (even on public routes) for convenience queries.
|
|
226
|
+
*/
|
|
227
|
+
supabase: SupabaseClient;
|
|
228
|
+
/**
|
|
229
|
+
* The authenticated user, or `null` for public (non-auth) routes.
|
|
230
|
+
* Guaranteed non-null when `requireAuth: true`.
|
|
231
|
+
*/
|
|
232
|
+
user: Awaited<ReturnType<SupabaseClient["auth"]["getUser"]>>["data"]["user"];
|
|
233
|
+
}
|
|
139
234
|
|
|
140
|
-
export { type ActionResponse, type MiddlewareOptions, type RouteConfig, type SupaError, createAction, withSupaAuth };
|
|
235
|
+
export { type ActionResponse, type MiddlewareOptions, type RouteConfig, type RouteHandlerContext, type RouteWrapperOptions, type SupaError, createAction, routeWrapper, withSupaAuth };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -57,6 +57,16 @@ interface MiddlewareOptions {
|
|
|
57
57
|
* @default "/login"
|
|
58
58
|
*/
|
|
59
59
|
redirectTo?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Explicit Supabase Project URL.
|
|
62
|
+
* If omitted, falls back to `process.env.NEXT_PUBLIC_SUPABASE_URL`.
|
|
63
|
+
*/
|
|
64
|
+
supabaseUrl?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Explicit Supabase Anon Key.
|
|
67
|
+
* If omitted, falls back to `process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY`.
|
|
68
|
+
*/
|
|
69
|
+
supabaseAnonKey?: string;
|
|
60
70
|
/**
|
|
61
71
|
* Strategy for extracting the user's role from the Supabase user object.
|
|
62
72
|
*
|
|
@@ -80,6 +90,40 @@ interface MiddlewareOptions {
|
|
|
80
90
|
role?: string | string[];
|
|
81
91
|
}) => void | Promise<void>;
|
|
82
92
|
}
|
|
93
|
+
/** Options for the `routeWrapper` higher-order function. */
|
|
94
|
+
interface RouteWrapperOptions {
|
|
95
|
+
/**
|
|
96
|
+
* If `true`, the wrapper will initialise a Supabase server client,
|
|
97
|
+
* verify the session via `getUser()`, and reject with 401 if invalid.
|
|
98
|
+
* The `context.supabase` and `context.user` are guaranteed non-null.
|
|
99
|
+
*
|
|
100
|
+
* @default false
|
|
101
|
+
*/
|
|
102
|
+
requireAuth?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Explicit Supabase Project URL.
|
|
105
|
+
* If omitted, falls back to `process.env.NEXT_PUBLIC_SUPABASE_URL`.
|
|
106
|
+
*/
|
|
107
|
+
supabaseUrl?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Explicit Supabase Anon Key.
|
|
110
|
+
* If omitted, falls back to `process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY`.
|
|
111
|
+
*/
|
|
112
|
+
supabaseAnonKey?: string;
|
|
113
|
+
}
|
|
114
|
+
/** Options for the `createAction` wrapper. */
|
|
115
|
+
interface ServerActionOptions {
|
|
116
|
+
/**
|
|
117
|
+
* Explicit Supabase Project URL.
|
|
118
|
+
* If omitted, falls back to `process.env.NEXT_PUBLIC_SUPABASE_URL`.
|
|
119
|
+
*/
|
|
120
|
+
supabaseUrl?: string;
|
|
121
|
+
/**
|
|
122
|
+
* Explicit Supabase Anon Key.
|
|
123
|
+
* If omitted, falls back to `process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY`.
|
|
124
|
+
*/
|
|
125
|
+
supabaseAnonKey?: string;
|
|
126
|
+
}
|
|
83
127
|
|
|
84
128
|
/**
|
|
85
129
|
* Create a Next.js middleware handler that protects routes with
|
|
@@ -135,6 +179,57 @@ declare function withSupaAuth(options: MiddlewareOptions): (request: NextRequest
|
|
|
135
179
|
* if (result.error) { ... }
|
|
136
180
|
* ```
|
|
137
181
|
*/
|
|
138
|
-
declare function createAction<TArgs extends unknown[], TResult>(fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult
|
|
182
|
+
declare function createAction<TArgs extends unknown[], TResult>(fn: (supabase: SupabaseClient, ...args: TArgs) => Promise<TResult>, options?: ServerActionOptions): (...args: TArgs) => Promise<ActionResponse<TResult>>;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Higher-order function that wraps a Next.js App Router Route Handler
|
|
186
|
+
* with standardized error handling and optional authentication gating.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* // app/api/posts/route.ts
|
|
191
|
+
* import { routeWrapper } from "next-supa-utils/server";
|
|
192
|
+
*
|
|
193
|
+
* // Public endpoint (no auth required)
|
|
194
|
+
* export const GET = routeWrapper(async (request) => {
|
|
195
|
+
* const data = await fetchPosts();
|
|
196
|
+
* return NextResponse.json({ data });
|
|
197
|
+
* });
|
|
198
|
+
*
|
|
199
|
+
* // Protected endpoint (requires valid Supabase session)
|
|
200
|
+
* export const POST = routeWrapper(
|
|
201
|
+
* async (request, { supabase, user }) => {
|
|
202
|
+
* const body = await request.json();
|
|
203
|
+
* const { data, error } = await supabase
|
|
204
|
+
* .from("posts")
|
|
205
|
+
* .insert({ ...body, user_id: user.id })
|
|
206
|
+
* .select()
|
|
207
|
+
* .single();
|
|
208
|
+
*
|
|
209
|
+
* if (error) throw error;
|
|
210
|
+
* return NextResponse.json({ data }, { status: 201 });
|
|
211
|
+
* },
|
|
212
|
+
* { requireAuth: true },
|
|
213
|
+
* );
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
declare function routeWrapper<TContext extends Record<string, unknown> = Record<string, unknown>>(handler: (request: NextRequest, context: RouteHandlerContext<TContext>) => Promise<NextResponse | Response>, options?: RouteWrapperOptions): (request: NextRequest, routeContext?: {
|
|
217
|
+
params?: Promise<TContext>;
|
|
218
|
+
}) => Promise<NextResponse>;
|
|
219
|
+
/** Context object passed to the route handler by `routeWrapper`. */
|
|
220
|
+
interface RouteHandlerContext<TParams extends Record<string, unknown> = Record<string, unknown>> {
|
|
221
|
+
/** Resolved dynamic route params (e.g. `{ id: "123" }`). */
|
|
222
|
+
params: TParams;
|
|
223
|
+
/**
|
|
224
|
+
* Supabase server client.
|
|
225
|
+
* Always available (even on public routes) for convenience queries.
|
|
226
|
+
*/
|
|
227
|
+
supabase: SupabaseClient;
|
|
228
|
+
/**
|
|
229
|
+
* The authenticated user, or `null` for public (non-auth) routes.
|
|
230
|
+
* Guaranteed non-null when `requireAuth: true`.
|
|
231
|
+
*/
|
|
232
|
+
user: Awaited<ReturnType<SupabaseClient["auth"]["getUser"]>>["data"]["user"];
|
|
233
|
+
}
|
|
139
234
|
|
|
140
|
-
export { type ActionResponse, type MiddlewareOptions, type RouteConfig, type SupaError, createAction, withSupaAuth };
|
|
235
|
+
export { type ActionResponse, type MiddlewareOptions, type RouteConfig, type RouteHandlerContext, type RouteWrapperOptions, type SupaError, createAction, routeWrapper, withSupaAuth };
|