@next-safe-action/adapter-better-auth 0.0.0 → 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Edoardo Ranghieri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ <div align="center">
2
+ <img src="https://raw.githubusercontent.com/next-safe-action/next-safe-action/main/assets/logo.png" alt="next-safe-action logo" width="36" height="36">
3
+ <a href="https://github.com/next-safe-action/next-safe-action/packages/adapter-better-auth"><h1>adapter-better-auth</h1></a>
4
+ </div>
5
+
6
+ This adapter offers a way to seamlessly integrate [next-safe-action](https://github.com/next-safe-action/next-safe-action) with [Better Auth](https://www.better-auth.com). It provides a `betterAuthMiddleware()` function that fetches the session, blocks unauthenticated requests, and injects fully-typed `{ user, session }` data into the action context.
7
+
8
+ ## Requirements
9
+
10
+ - Next.js >= `15.1.0`
11
+ - next-safe-action >= `8.4.0`
12
+ - better-auth >= `1.5.0`
13
+
14
+ ## Installation
15
+
16
+ ```sh
17
+ npm i next-safe-action better-auth @next-safe-action/adapter-better-auth
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ ### 1. Set up Better Auth
23
+
24
+ Create your Better Auth server instance:
25
+
26
+ ```ts
27
+ // src/lib/auth.ts
28
+ import { betterAuth } from "better-auth";
29
+
30
+ export const auth = betterAuth({
31
+ // ...your config (database, plugins, etc.)
32
+ });
33
+ ```
34
+
35
+ ### 2. Create an authenticated action client
36
+
37
+ ```ts
38
+ // src/lib/safe-action.ts
39
+ import { createSafeActionClient } from "next-safe-action";
40
+ import { betterAuthMiddleware } from "@next-safe-action/adapter-better-auth";
41
+ import { auth } from "./auth";
42
+
43
+ export const actionClient = createSafeActionClient();
44
+
45
+ export const authClient = actionClient.use(betterAuthMiddleware(auth));
46
+ ```
47
+
48
+ ### 3. Use it in your actions
49
+
50
+ ```ts
51
+ // src/app/actions.ts
52
+ "use server";
53
+
54
+ import { z } from "zod";
55
+ import { authClient } from "@/lib/safe-action";
56
+
57
+ export const updateProfile = authClient
58
+ .inputSchema(z.object({ name: z.string().min(1) }))
59
+ .action(async ({ parsedInput, ctx }) => {
60
+ // ctx.auth.user and ctx.auth.session are fully typed
61
+ const userId = ctx.auth.user.id;
62
+
63
+ await db.user.update({
64
+ where: { id: userId },
65
+ data: { name: parsedInput.name },
66
+ });
67
+
68
+ return { success: true };
69
+ });
70
+ ```
71
+
72
+ ## How it works
73
+
74
+ `betterAuthMiddleware()` creates a pre-validation middleware for the safe action client's `.use()` chain:
75
+
76
+ 1. **Fetches the session** by calling `auth.api.getSession({ headers: await headers() })`
77
+ 2. **Blocks unauthenticated requests** by calling `unauthorized()` from `next/navigation` when no session exists
78
+ 3. **Injects typed context** by passing `{ auth: { user, session } }` to `next()`, merging it into the action context
79
+
80
+ ### `unauthorized()` and auth interrupts
81
+
82
+ The default behavior uses `unauthorized()` from `next/navigation`, which requires `experimental.authInterrupts` in your Next.js configuration:
83
+
84
+ ```ts
85
+ // next.config.ts
86
+ import type { NextConfig } from "next";
87
+
88
+ const nextConfig: NextConfig = {
89
+ experimental: {
90
+ authInterrupts: true,
91
+ },
92
+ };
93
+
94
+ export default nextConfig;
95
+ ```
96
+
97
+ ## Custom authorization
98
+
99
+ Pass an `authorize` callback to customize the authorization flow. The session is pre-fetched and passed to the callback:
100
+
101
+ ```ts
102
+ import { unauthorized } from "next/navigation";
103
+ import { betterAuthMiddleware } from "@next-safe-action/adapter-better-auth";
104
+ import { auth } from "./auth";
105
+
106
+ // Role-based access
107
+ export const adminClient = actionClient.use(
108
+ betterAuthMiddleware(auth, {
109
+ authorize: ({ sessionData, next }) => {
110
+ if (!sessionData || sessionData.user.role !== "admin") {
111
+ unauthorized();
112
+ }
113
+ return next({ ctx: { auth: sessionData } });
114
+ },
115
+ })
116
+ );
117
+ ```
118
+
119
+ ### `authorize` callback parameters
120
+
121
+ - `auth`: the Better Auth server instance
122
+ - `sessionData`: the pre-fetched session data (`{ user, session } | null`)
123
+ - `ctx`: the current action context from preceding middleware
124
+ - `next`: call this to continue the middleware chain, pass `{ ctx }` to inject context
125
+
126
+ ## Server Action cookies
127
+
128
+ If your actions call Better Auth functions that set cookies (e.g. `signInEmail`, `signUpEmail`), add the `nextCookies()` plugin to your Better Auth instance. Refer to the [Better Auth documentation](https://better-auth.com/docs/integrations/next#server-action-cookies) for more details.
129
+
130
+ ```ts
131
+ // src/lib/auth.ts
132
+ import { betterAuth } from "better-auth";
133
+ import { nextCookies } from "better-auth/next-js";
134
+
135
+ export const auth = betterAuth({
136
+ // ...your config
137
+ plugins: [
138
+ // ...other plugins
139
+ nextCookies(), // must be the last plugin in the array
140
+ ],
141
+ });
142
+ ```
143
+
144
+ ## API reference
145
+
146
+ ### `betterAuthMiddleware(auth, opts?)`
147
+
148
+ Creates a middleware function for use with the safe action client's `.use()` method.
149
+
150
+ **Parameters:**
151
+
152
+ - `auth`: the Better Auth server instance (return value of `betterAuth()`)
153
+ - `opts?`: optional object with an `authorize` callback for custom authorization logic
154
+
155
+ **Returns:** a middleware function compatible with `.use()`
156
+
157
+ ### Exported types
158
+
159
+ - `BetterAuthContext<Options>`: the context shape added by the middleware (`{ auth: { user, session } }`)
160
+ - `AuthorizeFn<Options, NextCtx>`: the `authorize` callback signature
161
+ - `BetterAuthMiddlewareOpts<Options, NextCtx>`: the options object type for `betterAuthMiddleware`
162
+
163
+ ## Documentation
164
+
165
+ For full documentation, visit [next-safe-action.dev/docs/integrations/better-auth](https://next-safe-action.dev/docs/integrations/better-auth).
166
+
167
+ ## License
168
+
169
+ MIT
@@ -0,0 +1,62 @@
1
+ import { MiddlewareFn, MiddlewareResult } from "next-safe-action";
2
+ import { Auth, BetterAuthOptions } from "better-auth";
3
+
4
+ //#region src/index.types.d.ts
5
+ /**
6
+ * The default context shape added by `betterAuthMiddleware`.
7
+ * Contains `auth.user` and `auth.session`, fully typed from the better-auth instance.
8
+ */
9
+ type BetterAuthContext<O extends BetterAuthOptions> = {
10
+ auth: Auth<O>["$Infer"]["Session"];
11
+ };
12
+ /**
13
+ * Authorize callback signature for custom authorization logic.
14
+ * Receives the pre-fetched session data, current context, and the `next` function.
15
+ */
16
+ type AuthorizeFn<O extends BetterAuthOptions, NC extends object, Ctx extends object = object> = (args: {
17
+ sessionData: Auth<O>["$Infer"]["Session"] | null;
18
+ ctx: Ctx;
19
+ next: <C extends object>(opts?: {
20
+ ctx?: C;
21
+ }) => Promise<MiddlewareResult<any, C>>;
22
+ }) => Promise<MiddlewareResult<any, NC>>;
23
+ /**
24
+ * Options for `betterAuthMiddleware`.
25
+ */
26
+ type BetterAuthMiddlewareOpts<O extends BetterAuthOptions, NC extends object, Ctx extends object = object> = {
27
+ authorize: AuthorizeFn<O, NC, Ctx>;
28
+ };
29
+ //#endregion
30
+ //#region src/index.d.ts
31
+ /**
32
+ * Creates a next-safe-action middleware that integrates with better-auth.
33
+ *
34
+ * Default behavior: fetches the session via `auth.api.getSession()`, calls `unauthorized()` if
35
+ * no session exists, and injects `{ auth: { user, session } }` into the action context.
36
+ *
37
+ * Pass an `authorize` callback to customize the authorization flow. The session is pre-fetched
38
+ * and passed to the callback, so common customizations (e.g. role checks) don't need to re-fetch.
39
+ *
40
+ * Note: `unauthorized()` requires `experimental.authInterrupts: true` in your `next.config.ts` file.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * // Default: fetch session, unauthorized() if absent
45
+ * actionClient.use(betterAuthMiddleware(auth));
46
+ *
47
+ * // Custom: check role
48
+ * actionClient.use(betterAuthMiddleware(auth, {
49
+ * authorize: ({ sessionData, next }) => {
50
+ * if (!sessionData || sessionData.user.role !== "admin") {
51
+ * unauthorized();
52
+ * }
53
+ * return next({ ctx: { auth: sessionData } });
54
+ * },
55
+ * }));
56
+ * ```
57
+ */
58
+ declare function betterAuthMiddleware<O extends BetterAuthOptions>(auth: Auth<O>): MiddlewareFn<any, any, object, BetterAuthContext<O>>;
59
+ declare function betterAuthMiddleware<O extends BetterAuthOptions, NC extends object, Ctx extends object>(auth: Auth<O>, opts: BetterAuthMiddlewareOpts<O, NC, Ctx>): MiddlewareFn<any, any, Ctx, NC>;
60
+ //#endregion
61
+ export { type AuthorizeFn, type BetterAuthContext, type BetterAuthMiddlewareOpts, betterAuthMiddleware };
62
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,23 @@
1
+ import { createMiddleware } from "next-safe-action";
2
+ import { headers } from "next/headers.js";
3
+ import { unauthorized } from "next/navigation.js";
4
+ //#region src/index.ts
5
+ function betterAuthMiddleware(auth, opts) {
6
+ return createMiddleware().define(async ({ ctx, next }) => {
7
+ const sessionData = await auth.api.getSession({ headers: await headers() });
8
+ if (opts?.authorize) return opts.authorize({
9
+ sessionData,
10
+ ctx,
11
+ next
12
+ });
13
+ if (!sessionData) unauthorized();
14
+ return next({ ctx: { auth: {
15
+ user: sessionData.user,
16
+ session: sessionData.session
17
+ } } });
18
+ });
19
+ }
20
+ //#endregion
21
+ export { betterAuthMiddleware };
22
+
23
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Auth, BetterAuthOptions } from \"better-auth\";\nimport { createMiddleware } from \"next-safe-action\";\nimport type { MiddlewareFn } from \"next-safe-action\";\nimport { headers } from \"next/headers\";\nimport { unauthorized } from \"next/navigation\";\nimport type { BetterAuthContext, BetterAuthMiddlewareOpts } from \"./index.types\";\n\n/**\n * Creates a next-safe-action middleware that integrates with better-auth.\n *\n * Default behavior: fetches the session via `auth.api.getSession()`, calls `unauthorized()` if\n * no session exists, and injects `{ auth: { user, session } }` into the action context.\n *\n * Pass an `authorize` callback to customize the authorization flow. The session is pre-fetched\n * and passed to the callback, so common customizations (e.g. role checks) don't need to re-fetch.\n *\n * Note: `unauthorized()` requires `experimental.authInterrupts: true` in your `next.config.ts` file.\n *\n * @example\n * ```ts\n * // Default: fetch session, unauthorized() if absent\n * actionClient.use(betterAuthMiddleware(auth));\n *\n * // Custom: check role\n * actionClient.use(betterAuthMiddleware(auth, {\n * authorize: ({ sessionData, next }) => {\n * if (!sessionData || sessionData.user.role !== \"admin\") {\n * unauthorized();\n * }\n * return next({ ctx: { auth: sessionData } });\n * },\n * }));\n * ```\n */\nexport function betterAuthMiddleware<O extends BetterAuthOptions>(\n\tauth: Auth<O>\n): MiddlewareFn<any, any, object, BetterAuthContext<O>>;\nexport function betterAuthMiddleware<O extends BetterAuthOptions, NC extends object, Ctx extends object>(\n\tauth: Auth<O>,\n\topts: BetterAuthMiddlewareOpts<O, NC, Ctx>\n): MiddlewareFn<any, any, Ctx, NC>;\nexport function betterAuthMiddleware<O extends BetterAuthOptions>(\n\tauth: Auth<O>,\n\topts?: BetterAuthMiddlewareOpts<O, any, any>\n) {\n\treturn createMiddleware().define(async ({ ctx, next }) => {\n\t\tconst sessionData = await auth.api.getSession({ headers: await headers() });\n\n\t\tif (opts?.authorize) {\n\t\t\treturn opts.authorize({ sessionData, ctx, next });\n\t\t}\n\n\t\tif (!sessionData) {\n\t\t\tunauthorized();\n\t\t}\n\n\t\treturn next({ ctx: { auth: { user: sessionData.user, session: sessionData.session } } });\n\t});\n}\n\nexport type { AuthorizeFn, BetterAuthContext, BetterAuthMiddlewareOpts } from \"./index.types\";\n"],"mappings":";;;;AAyCA,SAAgB,qBACf,MACA,MACC;AACD,QAAO,kBAAkB,CAAC,OAAO,OAAO,EAAE,KAAK,WAAW;EACzD,MAAM,cAAc,MAAM,KAAK,IAAI,WAAW,EAAE,SAAS,MAAM,SAAS,EAAE,CAAC;AAE3E,MAAI,MAAM,UACT,QAAO,KAAK,UAAU;GAAE;GAAa;GAAK;GAAM,CAAC;AAGlD,MAAI,CAAC,YACJ,eAAc;AAGf,SAAO,KAAK,EAAE,KAAK,EAAE,MAAM;GAAE,MAAM,YAAY;GAAM,SAAS,YAAY;GAAS,EAAE,EAAE,CAAC;GACvF"}
package/package.json CHANGED
@@ -1,15 +1,89 @@
1
1
  {
2
2
  "name": "@next-safe-action/adapter-better-auth",
3
- "version": "0.0.0",
4
- "description": "wip",
5
- "main": "index.js",
6
- "scripts": {},
7
- "keywords": [],
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "description": "Better Auth adapter for next-safe-action.",
6
+ "main": "./dist/index.mjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.mts",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.mts",
15
+ "default": "./dist/index.mjs"
16
+ }
17
+ },
18
+ "typesVersions": {
19
+ "*": {
20
+ ".": [
21
+ "./dist/index.d.mts"
22
+ ]
23
+ }
24
+ },
25
+ "funding": [
26
+ {
27
+ "type": "github",
28
+ "url": "https://github.com/sponsors/TheEdoRan"
29
+ },
30
+ {
31
+ "type": "paypal",
32
+ "url": "https://www.paypal.com/donate/?hosted_button_id=ES9JRPSC66XKW"
33
+ }
34
+ ],
35
+ "keywords": [
36
+ "next",
37
+ "nextjs",
38
+ "react",
39
+ "rsc",
40
+ "react server components",
41
+ "mutation",
42
+ "action",
43
+ "actions",
44
+ "react actions",
45
+ "next actions",
46
+ "server actions",
47
+ "next-safe-action",
48
+ "next safe action",
49
+ "better-auth",
50
+ "better auth",
51
+ "auth",
52
+ "authentication"
53
+ ],
8
54
  "author": "Edoardo Ranghieri",
9
55
  "license": "MIT",
10
- "packageManager": "pnpm@10.33.0",
11
- "private": false,
56
+ "devDependencies": {
57
+ "@types/node": "^24",
58
+ "better-auth": "^1.5.6",
59
+ "next": "^16",
60
+ "oxlint": "^1.57.0",
61
+ "oxlint-tsgolint": "^0.15.0",
62
+ "tsdown": "^0.21.0",
63
+ "typescript": "^6.0.2",
64
+ "vitest": "^3.1.1",
65
+ "zod": "^4.3.6",
66
+ "next-safe-action": "8.4.0"
67
+ },
68
+ "peerDependencies": {
69
+ "better-auth": ">= 1.5.0",
70
+ "next": ">= 15.1.0",
71
+ "next-safe-action": ">= 8.4.0"
72
+ },
73
+ "engines": {
74
+ "node": ">=18.17"
75
+ },
76
+ "repository": {
77
+ "type": "git",
78
+ "url": "https://github.com/next-safe-action/next-safe-action.git",
79
+ "directory": "packages/adapter-better-auth"
80
+ },
12
81
  "publishConfig": {
13
82
  "access": "public"
83
+ },
84
+ "scripts": {
85
+ "test": "vitest run",
86
+ "lint": "tsc --noEmit && oxlint --type-aware .",
87
+ "build": "tsdown"
14
88
  }
15
- }
89
+ }
package/index.js DELETED
@@ -1 +0,0 @@
1
- export {}