@take-out/better-auth-utils 0.0.66 → 0.0.68

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.
@@ -0,0 +1,166 @@
1
+ import { createRemoteJWKSet, jwtVerify } from "jose";
2
+ function _assert_this_initialized(self) {
3
+ if (self === void 0) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
4
+ return self;
5
+ }
6
+ function _call_super(_this, derived, args) {
7
+ return derived = _get_prototype_of(derived), _possible_constructor_return(_this, _is_native_reflect_construct() ? Reflect.construct(derived, args || [], _get_prototype_of(_this).constructor) : derived.apply(_this, args));
8
+ }
9
+ function _class_call_check(instance, Constructor) {
10
+ if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function");
11
+ }
12
+ function _construct(Parent, args, Class) {
13
+ return _is_native_reflect_construct() ? _construct = Reflect.construct : _construct = function (Parent2, args2, Class2) {
14
+ var a = [null];
15
+ a.push.apply(a, args2);
16
+ var Constructor = Function.bind.apply(Parent2, a),
17
+ instance = new Constructor();
18
+ return Class2 && _set_prototype_of(instance, Class2.prototype), instance;
19
+ }, _construct.apply(null, arguments);
20
+ }
21
+ function _get_prototype_of(o) {
22
+ return _get_prototype_of = Object.setPrototypeOf ? Object.getPrototypeOf : function (o2) {
23
+ return o2.__proto__ || Object.getPrototypeOf(o2);
24
+ }, _get_prototype_of(o);
25
+ }
26
+ function _inherits(subClass, superClass) {
27
+ if (typeof superClass != "function" && superClass !== null) throw new TypeError("Super expression must either be null or a function");
28
+ subClass.prototype = Object.create(superClass && superClass.prototype, {
29
+ constructor: {
30
+ value: subClass,
31
+ writable: !0,
32
+ configurable: !0
33
+ }
34
+ }), superClass && _set_prototype_of(subClass, superClass);
35
+ }
36
+ function _instanceof(left, right) {
37
+ return right != null && typeof Symbol < "u" && right[Symbol.hasInstance] ? !!right[Symbol.hasInstance](left) : left instanceof right;
38
+ }
39
+ function _is_native_function(fn) {
40
+ return Function.toString.call(fn).indexOf("[native code]") !== -1;
41
+ }
42
+ function _possible_constructor_return(self, call) {
43
+ return call && (_type_of(call) === "object" || typeof call == "function") ? call : _assert_this_initialized(self);
44
+ }
45
+ function _set_prototype_of(o, p) {
46
+ return _set_prototype_of = Object.setPrototypeOf || function (o2, p2) {
47
+ return o2.__proto__ = p2, o2;
48
+ }, _set_prototype_of(o, p);
49
+ }
50
+ function _type_of(obj) {
51
+ "@swc/helpers - typeof";
52
+
53
+ return obj && typeof Symbol < "u" && obj.constructor === Symbol ? "symbol" : typeof obj;
54
+ }
55
+ function _wrap_native_super(Class) {
56
+ var _cache = typeof Map == "function" ? /* @__PURE__ */new Map() : void 0;
57
+ return _wrap_native_super = function (Class2) {
58
+ if (Class2 === null || !_is_native_function(Class2)) return Class2;
59
+ if (typeof Class2 != "function") throw new TypeError("Super expression must either be null or a function");
60
+ if (typeof _cache < "u") {
61
+ if (_cache.has(Class2)) return _cache.get(Class2);
62
+ _cache.set(Class2, Wrapper);
63
+ }
64
+ function Wrapper() {
65
+ return _construct(Class2, arguments, _get_prototype_of(this).constructor);
66
+ }
67
+ return Wrapper.prototype = Object.create(Class2.prototype, {
68
+ constructor: {
69
+ value: Wrapper,
70
+ enumerable: !1,
71
+ writable: !0,
72
+ configurable: !0
73
+ }
74
+ }), _set_prototype_of(Wrapper, Class2);
75
+ }, _wrap_native_super(Class);
76
+ }
77
+ function _is_native_reflect_construct() {
78
+ try {
79
+ var result = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
80
+ } catch {}
81
+ return (_is_native_reflect_construct = function () {
82
+ return !!result;
83
+ })();
84
+ }
85
+ var NotAuthenticatedError = /* @__PURE__ */function (Error1) {
86
+ "use strict";
87
+
88
+ _inherits(NotAuthenticatedError2, Error1);
89
+ function NotAuthenticatedError2() {
90
+ return _class_call_check(this, NotAuthenticatedError2), _call_super(this, NotAuthenticatedError2, arguments);
91
+ }
92
+ return NotAuthenticatedError2;
93
+ }(_wrap_native_super(Error)),
94
+ InvalidTokenError = /* @__PURE__ */function (Error1) {
95
+ "use strict";
96
+
97
+ _inherits(InvalidTokenError2, Error1);
98
+ function InvalidTokenError2() {
99
+ return _class_call_check(this, InvalidTokenError2), _call_super(this, InvalidTokenError2, arguments);
100
+ }
101
+ return InvalidTokenError2;
102
+ }(_wrap_native_super(Error));
103
+ async function getAuthDataFromRequest(authServer, req, tokenOptions) {
104
+ var authHeader = req.headers.get("authorization"),
105
+ cookie = authHeader?.split("Bearer ")[1],
106
+ newHeaders = new Headers(req.headers);
107
+ cookie && newHeaders.set("Cookie", cookie);
108
+ try {
109
+ var session = await authServer.api.getSession({
110
+ headers: newHeaders
111
+ });
112
+ if (session?.user) return {
113
+ id: session.user.id,
114
+ email: session.user.email || void 0,
115
+ role: session.user.role === "admin" ? "admin" : void 0
116
+ };
117
+ } catch {}
118
+ var jwtToken = authHeader?.replace("Bearer ", "");
119
+ if (jwtToken) try {
120
+ var payload = await validateToken(jwtToken, tokenOptions),
121
+ userId = payload?.id || payload?.sub;
122
+ if (userId) return {
123
+ id: userId,
124
+ email: payload.email,
125
+ role: payload.role === "admin" ? "admin" : void 0
126
+ };
127
+ } catch (err) {
128
+ if (!_instanceof(err, InvalidTokenError)) throw err;
129
+ }
130
+ return null;
131
+ }
132
+ async function validateToken(token, options) {
133
+ var {
134
+ baseUrl = process.env.ONE_SERVER_URL,
135
+ forceIssuer = process.env.FORCE_ISSUER || "",
136
+ jwksPath = "/api/auth/jwks"
137
+ } = options || {};
138
+ if (!baseUrl) throw new Error("No baseURL!");
139
+ var normalizedBaseUrl = removeTrailingSlash(baseUrl),
140
+ url = `${forceIssuer || normalizedBaseUrl}${jwksPath}`,
141
+ JWKS = createRemoteJWKSet(new URL(url));
142
+ try {
143
+ var verifyOptions = forceIssuer ? {} : {
144
+ issuer: normalizedBaseUrl,
145
+ audience: normalizedBaseUrl
146
+ },
147
+ {
148
+ payload
149
+ } = await jwtVerify(token, JWKS, verifyOptions);
150
+ return payload;
151
+ } catch (error) {
152
+ throw new InvalidTokenError(`${error}`);
153
+ }
154
+ }
155
+ async function isValidJWT(token, options) {
156
+ try {
157
+ return await validateToken(token, options), !0;
158
+ } catch {
159
+ return !1;
160
+ }
161
+ }
162
+ function removeTrailingSlash(str) {
163
+ return str.replace(/\/$/, "");
164
+ }
165
+ export { InvalidTokenError, NotAuthenticatedError, getAuthDataFromRequest, isValidJWT, validateToken };
166
+ //# sourceMappingURL=server.native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["createRemoteJWKSet","jwtVerify","_assert_this_initialized","self","ReferenceError","_call_super","_this","derived","args","_get_prototype_of","_possible_constructor_return","_is_native_reflect_construct","Reflect","construct","constructor","apply","_class_call_check","instance","Constructor","TypeError","_construct","Parent","Class","Parent2","args2","Class2","a","push","Function","bind","_set_prototype_of","prototype","arguments","o","Object","setPrototypeOf","getPrototypeOf","o2","__proto__","_inherits","subClass","superClass","create","value","writable","configurable","_instanceof","left","right","Symbol","hasInstance","_is_native_function","fn","toString","call","indexOf","_type_of","p","p2","obj","_wrap_native_super","_cache","Map"],"sources":["../../src/server.ts"],"sourcesContent":[null],"mappings":"AAMA,SAASA,kBAAA,EAAoBC,SAAA,QAAkC;AAiBxD,SAAMC,yBAAAC,IAA8B;EAAO,IAAAA,IAAA,aAC3C,MAAM,IAAAC,cAAA,4DAAgC;EAAC,OAAAD,IAAA;AAiB9C;AAQE,SAAME,YAAaC,KAAI,EAAAC,OAAQ,EAAIC,IAAA;EAI/B,OAAAD,OACF,GAAAE,iBAAe,CAAUF,OAAM,GAAAG,4BAAA,CAAAJ,KAAA,EAAAK,4BAAA,KAAAC,OAAA,CAAAC,SAAA,CAAAN,OAAA,EAAAC,IAAA,QAAAC,iBAAA,CAAAH,KAAA,EAAAQ,WAAA,IAAAP,OAAA,CAAAQ,KAAA,CAAAT,KAAA,EAAAE,IAAA;AAIjC;AACE,SAAAQ,iBAAsBA,CAAAC,QAAA,EAAWC,WAAI;EACrC,MAAID,QAAA,YAASC,WAAA,GACX,UAAOC,SAAA;AAAA;AACY,SACjBC,UAAOA,CAAAC,MAAQ,EAAKb,IAAA,EAAAc,KAAS;EAAA,OAC7BX,4BAA4B,KAAAS,UAAU,GAAAR,OAAU,CAAAC,SAAA,GAAAO,UAAA,YAAAA,CAAAG,OAAA,EAAAC,KAAA,EAAAC,MAAA;IAAA,IAClDC,CAAA,IAEJ,IAAQ,CAER;IAIAA,CAAA,CAAAC,IAAM,CAAAZ,KAAA,CAAAW,CAAA,EAAWF,KAAA;IAEjB,IAAIN,WAAA,GAAAU,QAAA,CAAAC,IAAA,CAAAd,KAAA,CAAAQ,OAAA,EAAAG,CAAA;MAAAT,QAAA,OAAAC,WAAA;IACF,OAAIO,MAAA,IAAAK,iBAAA,CAAAb,QAAA,EAAAQ,MAAA,CAAAM,SAAA,GAAAd,QAAA;EACF,GAAAG,UAAM,CAAAL,KAAA,CAAU,MAAMiB,SAAA;AAEtB;AACE,SAAAvB,iBAAOA,CAAAwB,CAAA;EAAA,OAAAxB,iBACD,GAAAyB,MAAA,CAAAC,cAAA,GAAAD,MAAA,CAAAE,cAAA,aAAAC,EAAA;IAAA,OACJA,EAAA,CAAAC,SAAQ,IAAgBJ,MAAA,CAAAE,cAAA,CAAAC,EAAA;EAAA,GAAA5B,iBACjB,CAAAwB,CAAgB;AAA6B;AACtD,SAEJM,SAAcA,CAAAC,QAAA,EAAAC,UAAA;EACZ,WAAMA,UAAA,IAAe,cAAAA,UAAA,WACnB,UAAMtB,SAAA;EAAAqB,QAEV,CAAAT,SAAA,GAAAG,MAAA,CAAAQ,MAAA,CAAAD,UAAA,IAAAA,UAAA,CAAAV,SAAA;IAGFjB,WAAO;MACT6B,KAAA,EAAAH,QAAA;MAIAI,QAAA,EAAsB;MAIpBC,YAAM;IACJ;EAAsB,EACtB,EAAAJ,UAAA,IAAcX,iBAAY,CAAAU,QAAgB,EAAAC,UAAA;AAAA;AAC/B,SACTK,WAAYA,CAAAC,IAAA,EAAAC,KAAA;EAEhB,OAAKA,KAAA,mBAAAC,MAAA,UAAAD,KAAA,CAAAC,MAAA,CAAAC,WAAA,MAAAF,KAAA,CAAAC,MAAA,CAAAC,WAAA,EAAAH,IAAA,IAAAA,IAAA,YAAAC,KAAA;AACH;AAGF,SAAMG,mBAAoBA,CAAAC,EAAA;EAM1B,OAAIxB,QAAA,CAAAyB,QAAA,CAAAC,IAAA,CAAAF,EAAA,EAAAG,OAAA;AACF;AAEI,SACE7C,4BAAQA,CAAAP,IAAA,EAAAmD,IAAA;EAAA,OACRA,IAAA,KAAUE,QAAA,CAAAF,IAAA,yBAAAA,IAAA,kBAAAA,IAAA,GAAApD,wBAAA,CAAAC,IAAA;AAAA;AAKhB,SAAA2B,iBAAOA,CAAAG,CAAA,EAAAwB,CAAA;EACT,OAAA3B,iBAAgB,GAAAI,MAAA,CAAAC,cAAA,cAAAE,EAAA,EAAAqB,EAAA;IACd,OAAMrB,EAAA,CAAIC,SAAA,GAAAoB,EAAA,EAAArB,EAAkB;EAC9B,GAAAP,iBAAA,CAAAG,CAAA,EAAAwB,CAAA;AACF;AAEA,SAAAD,QAAsBA,CAAAG,GAAA;EAIpB,uBAAI;;EACF,OAAAA,GAAA,WAAMV,MAAA,GAAc,OAAOU,GAAA,CAAA7C,WACpB,KAAAmC,MAAA,qBAAAU,GAAA;AAAA;AAEP,SAAAC,kBAAOA,CAAAtC,KAAA;EACT,IAAAuC,MAAA,UAAAC,GAAA,oCAAAA,GAAA;EACF,OAAAF,kBAAA,YAAAA,CAAAnC,MAAA;IAEA,IAAAA,MAAS,cAAA0B,mBAAiC,CAAA1B,MAAA,UAAAA,MAAA;IACxC,WAAWA,MAAQ,cAAS,EAC9B,UAAAN,SAAA","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@take-out/better-auth-utils",
3
- "version": "0.0.66",
3
+ "version": "0.0.68",
4
4
  "description": "Better auth utilities and client for React/React Native applications",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -32,17 +32,24 @@
32
32
  "import": "./dist/esm/index.mjs",
33
33
  "require": "./dist/cjs/index.cjs",
34
34
  "default": "./dist/esm/index.mjs"
35
+ },
36
+ "./server": {
37
+ "types": "./types/server.d.ts",
38
+ "import": "./dist/esm/server.mjs",
39
+ "require": "./dist/cjs/server.cjs",
40
+ "default": "./dist/esm/server.mjs"
35
41
  }
36
42
  },
37
43
  "dependencies": {
38
- "@take-out/helpers": "0.0.62",
39
- "better-auth": "^1.3.28"
44
+ "@take-out/helpers": "0.0.67",
45
+ "better-auth": "^1.3.28",
46
+ "jose": "^6.0.10"
40
47
  },
41
48
  "peerDependencies": {
42
49
  "react": "*"
43
50
  },
44
51
  "devDependencies": {
45
- "@tamagui/build": "2.0.0-rc.0-1769998500160",
52
+ "@tamagui/build": "2.0.0-rc.0-1770084657117",
46
53
  "@types/node": "24.0.3",
47
54
  "@types/react": "^19.0.8",
48
55
  "oxfmt": "^0.16.0",
@@ -2,11 +2,10 @@
2
2
  * Better-auth helpers for React / React Native applications
3
3
  *
4
4
  * Features:
5
- * - JWT support (for Zero, Tauri, React Native)
6
5
  * - Session persistence in local storage
7
- * - Token validation and refresh
8
6
  * - State management with emitters
9
7
  * - Automatic retry on errors
8
+ * - Optional JWT support (for Tauri, React Native)
10
9
  */
11
10
 
12
11
  import {
@@ -29,6 +28,7 @@ export type AuthState<U extends User = User> = {
29
28
  state: 'loading' | 'logged-in' | 'logged-out'
30
29
  session: Session | null
31
30
  user: U | null
31
+ /** JWT token - only populated when useJWT is enabled */
32
32
  token: string | null
33
33
  }
34
34
 
@@ -37,33 +37,45 @@ export interface BetterAuthClientProps<
37
37
  > extends BetterAuthClientOptions {
38
38
  /**
39
39
  * Callback to transform and type the user object
40
- * This allows you to add app-specific fields and ensure proper typing
41
40
  * @default (user) => user
42
41
  */
43
42
  createUser?: (user: User) => TUser
43
+
44
44
  /**
45
45
  * Optional callback when authentication state changes
46
46
  */
47
47
  onAuthStateChange?: (state: AuthState<TUser>) => void
48
+
48
49
  /**
49
50
  * Optional callback for handling auth errors
50
51
  */
51
52
  onAuthError?: (error: any) => void
53
+
52
54
  /**
53
55
  * Storage key prefix for local storage
54
56
  * @default 'auth'
55
57
  */
56
58
  storagePrefix?: string
59
+
57
60
  /**
58
61
  * Retry delay in milliseconds after auth errors
59
62
  * @default 4000
60
63
  */
61
64
  retryDelay?: number
65
+
62
66
  /**
63
- * Custom token validation endpoint
64
- * @default '/api/auth/validateToken'
67
+ * Enable JWT token management for native apps (Tauri, React Native)
68
+ * When false (default), auth uses session cookies forwarded by the server
69
+ * When true, fetches and manages JWT tokens for Authorization header auth
70
+ * @default false
65
71
  */
66
- tokenValidationEndpoint?: string
72
+ useJWT?: boolean
73
+
74
+ /**
75
+ * Cookie names to clear on auth invalidation
76
+ * @default ['better-auth.jwt', 'better-auth.session_token']
77
+ */
78
+ authCookieNames?: string[]
67
79
  }
68
80
 
69
81
  export interface BetterAuthClientReturn<U extends User = User, TClient = any> {
@@ -72,6 +84,7 @@ export interface BetterAuthClientReturn<U extends User = User, TClient = any> {
72
84
  authClient: TClient
73
85
  setAuthClientToken: (props: { token: string; session: string }) => Promise<void>
74
86
  clearAuthClientToken: () => void
87
+ clearAllAuth: () => void
75
88
  useAuth: () => AuthState<U>
76
89
  getAuth: () => AuthState<U> & { loggedIn: boolean }
77
90
  getValidToken: () => Promise<string | undefined>
@@ -95,7 +108,8 @@ export function createBetterAuthClient<const Opts extends BetterAuthClientProps<
95
108
  createUser,
96
109
  storagePrefix = 'auth',
97
110
  retryDelay = 4000,
98
- tokenValidationEndpoint = '/api/auth/validateToken',
111
+ useJWT = false,
112
+ authCookieNames = ['better-auth.jwt', 'better-auth.session_token'],
99
113
  ...authClientOptions
100
114
  } = options
101
115
 
@@ -106,33 +120,33 @@ export function createBetterAuthClient<const Opts extends BetterAuthClientProps<
106
120
  token: null,
107
121
  }
108
122
 
123
+ const keysStorage = createStorageValue<StorageKeys>(`${storagePrefix}-keys`)
124
+ const stateStorage = createStorageValue<AuthState<TUser>>(`${storagePrefix}-state`)
125
+
109
126
  const createAuthClientWithSession = (session: string) => {
110
127
  return createAuthClient({
111
128
  ...authClientOptions,
112
129
  fetchOptions: {
113
- headers: {
114
- Authorization: `Bearer ${session}`,
115
- },
130
+ credentials: 'include',
131
+ headers: useJWT ? { Authorization: `Bearer ${session}` } : undefined,
116
132
  },
117
133
  })
118
134
  }
119
135
 
120
- const keysStorage = createStorageValue<StorageKeys>(`${storagePrefix}-keys`)
121
- const stateStorage = createStorageValue<AuthState<TUser>>(`${storagePrefix}-state`)
122
-
123
136
  let authClient = (() => {
124
137
  const existingSession = keysStorage.get()?.session
125
138
  return existingSession
126
139
  ? createAuthClientWithSession(existingSession)
127
- : createAuthClient(authClientOptions as Opts)
140
+ : createAuthClient({
141
+ ...authClientOptions,
142
+ fetchOptions: { credentials: 'include' },
143
+ } as Opts)
128
144
  })()
129
145
 
130
146
  const authState = createEmitter<AuthState<TUser>>(
131
147
  'authState',
132
148
  stateStorage.get() || empty,
133
- {
134
- comparator: isEqualDeepLite,
135
- }
149
+ { comparator: isEqualDeepLite }
136
150
  )
137
151
 
138
152
  const authClientVersion = createEmitter<number>('authClientVersion', 0)
@@ -149,14 +163,15 @@ export function createBetterAuthClient<const Opts extends BetterAuthClientProps<
149
163
  token: next.token,
150
164
  session: next.session.token,
151
165
  })
152
- } else {
166
+ } else if (next.session) {
153
167
  keysStorage.set({
154
168
  token: '',
155
- session: '',
169
+ session: next.session.token,
156
170
  })
171
+ } else {
172
+ keysStorage.set({ token: '', session: '' })
157
173
  }
158
174
 
159
- // call optional callback
160
175
  onAuthStateChange?.(next)
161
176
  }
162
177
 
@@ -182,7 +197,6 @@ export function createBetterAuthClient<const Opts extends BetterAuthClientProps<
182
197
 
183
198
  if (error) {
184
199
  onAuthError?.(error)
185
- // retry by re-subscribing after a delay to recover from transient errors
186
200
  scheduleAuthRetry(retryDelay)
187
201
  return
188
202
  }
@@ -195,8 +209,6 @@ export function createBetterAuthClient<const Opts extends BetterAuthClientProps<
195
209
  }
196
210
 
197
211
  // if we have a persisted session but server hasn't confirmed yet, stay loading
198
- // this prevents redirect race on direct navigation while session is being validated
199
- // note: data === null means server confirmed no session, data === undefined means still loading
200
212
  const hasPersistedSession = !!keysStorage.get()?.session
201
213
  const nextState = isPending
202
214
  ? 'loading'
@@ -206,8 +218,7 @@ export function createBetterAuthClient<const Opts extends BetterAuthClientProps<
206
218
  ? 'loading'
207
219
  : 'logged-out'
208
220
 
209
- // only update session/user when we have definitive data (not during pending state)
210
- // this prevents clearing persisted data during the loading phase
221
+ // only update session/user when we have definitive data
211
222
  const sessionUpdate =
212
223
  nextState === 'loading'
213
224
  ? {}
@@ -216,13 +227,25 @@ export function createBetterAuthClient<const Opts extends BetterAuthClientProps<
216
227
  user: data?.user ? (createUser ? createUser(data.user) : data.user) : null,
217
228
  }
218
229
 
230
+ // detect new session
231
+ const previousSession = authState.value?.session
232
+ const isNewSession =
233
+ data?.session &&
234
+ (!previousSession ||
235
+ previousSession.id !== data.session.id ||
236
+ previousSession.userId !== data.session.userId)
237
+
219
238
  setState({
220
239
  state: nextState,
221
240
  ...sessionUpdate,
222
241
  })
223
242
 
224
- // get token on creating a new session (for native + tauri)
225
- if (data?.session && !authState.value.token) {
243
+ // fetch JWT token when useJWT is enabled (for native/tauri apps)
244
+ if (useJWT && data?.session && (isNewSession || !authState.value.token)) {
245
+ if (isNewSession && authState.value.token) {
246
+ setState({ token: null })
247
+ }
248
+
226
249
  getValidToken().then((token) => {
227
250
  if (token) {
228
251
  setState({ token })
@@ -233,9 +256,7 @@ export function createBetterAuthClient<const Opts extends BetterAuthClientProps<
233
256
  }
234
257
 
235
258
  function scheduleAuthRetry(delayMs: number) {
236
- if (retryTimer) {
237
- clearTimeout(retryTimer)
238
- }
259
+ if (retryTimer) clearTimeout(retryTimer)
239
260
  retryTimer = setTimeout(() => {
240
261
  retryTimer = null
241
262
  subscribeToAuthEffect()
@@ -243,47 +264,39 @@ export function createBetterAuthClient<const Opts extends BetterAuthClientProps<
243
264
  }
244
265
 
245
266
  async function getValidToken(): Promise<string | undefined> {
246
- const existing = keysStorage.get()?.token
247
-
248
- if (existing) {
249
- try {
250
- const response = await fetch(tokenValidationEndpoint, {
251
- method: 'POST',
252
- headers: {
253
- 'Content-Type': 'application/json',
254
- },
255
- body: JSON.stringify({
256
- token: existing,
257
- }),
258
- }).then((res) => res.json())
259
-
260
- if (response?.valid) {
261
- return existing
262
- }
263
- } catch (error) {
264
- console.error('Error validating token:', error)
265
- }
266
- }
267
-
268
267
  const res = await authClient.$fetch('/token')
269
268
  if (res.error) {
270
269
  console.error(`Error fetching token: ${res.error.statusText}`)
271
270
  return undefined
272
271
  }
273
- const data = res.data as any
274
- return data?.token as string | undefined
272
+ return (res.data as any)?.token as string | undefined
275
273
  }
276
274
 
277
275
  const clearAuthClientToken = () => {
278
276
  keysStorage.remove()
279
277
  }
280
278
 
279
+ function clearAuthCookies() {
280
+ if (typeof document === 'undefined') return
281
+
282
+ for (const cookieName of authCookieNames) {
283
+ document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
284
+ const domain = window.location.hostname
285
+ document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${domain}`
286
+ if (domain.startsWith('.')) {
287
+ document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${domain.slice(1)}`
288
+ }
289
+ }
290
+ }
291
+
292
+ function clearAllAuth() {
293
+ clearAuthCookies()
294
+ clearState()
295
+ }
296
+
281
297
  const getAuth = () => {
282
298
  const state = authState?.value || empty
283
- return {
284
- ...state,
285
- loggedIn: !!state.session,
286
- }
299
+ return { ...state, loggedIn: !!state.session }
287
300
  }
288
301
 
289
302
  const useAuth = () => {
@@ -296,16 +309,12 @@ export function createBetterAuthClient<const Opts extends BetterAuthClientProps<
296
309
  setState(empty)
297
310
  }
298
311
 
299
- // initialize subscription
300
312
  subscribeToAuthEffect()
301
313
 
302
- // cleanup on unmount
303
314
  if (typeof window !== 'undefined' && window.addEventListener) {
304
315
  const cleanup = () => {
305
316
  dispose?.()
306
- if (retryTimer) {
307
- clearTimeout(retryTimer)
308
- }
317
+ if (retryTimer) clearTimeout(retryTimer)
309
318
  }
310
319
  window.addEventListener('beforeunload', cleanup)
311
320
  }
@@ -315,19 +324,13 @@ export function createBetterAuthClient<const Opts extends BetterAuthClientProps<
315
324
  if (key === 'signOut') {
316
325
  return () => {
317
326
  clearState()
318
-
319
- // don't await - better-auth has fetch issues on react native
320
- // attempting to await causes "method uppercase not a func" error
321
- // @ts-expect-error better-auth type issue, signOut does exist
327
+ // @ts-expect-error better-auth type issue
322
328
  authClient.signOut?.()
323
-
324
- // force refresh to clear any remaining state
325
329
  if (typeof window !== 'undefined') {
326
330
  window.location?.reload?.()
327
331
  }
328
332
  }
329
333
  }
330
- // ensure always latest authClient after login
331
334
  return Reflect.get(authClient, key)
332
335
  },
333
336
  }) as ReturnType<typeof createAuthClient<Opts>>
@@ -339,6 +342,7 @@ export function createBetterAuthClient<const Opts extends BetterAuthClientProps<
339
342
  authClient: proxiedAuthClient,
340
343
  setAuthClientToken,
341
344
  clearAuthClientToken,
345
+ clearAllAuth,
342
346
  useAuth,
343
347
  getAuth,
344
348
  getValidToken,
package/src/server.ts ADDED
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Server-side auth utilities for better-auth
3
+ * - Session validation via cookies (web)
4
+ * - JWT validation via JWKS (native apps)
5
+ */
6
+
7
+ import { createRemoteJWKSet, jwtVerify, type JWTPayload } from 'jose'
8
+
9
+ export interface ValidateTokenOptions {
10
+ /** base URL for the auth server (e.g., https://myapp.com) */
11
+ baseUrl?: string
12
+ /** optional issuer override for CI/test environments */
13
+ forceIssuer?: string
14
+ /** JWKS endpoint path, defaults to /api/auth/jwks */
15
+ jwksPath?: string
16
+ }
17
+
18
+ export interface AuthData {
19
+ id: string
20
+ email?: string
21
+ role: 'admin' | undefined
22
+ }
23
+
24
+ export class NotAuthenticatedError extends Error {}
25
+ export class InvalidTokenError extends Error {}
26
+
27
+ export type AuthServer = {
28
+ api: {
29
+ getSession: (opts: { headers: Headers }) =>
30
+ | Promise<{
31
+ user: { id: string; email?: string | null; role?: string | null }
32
+ } | null>
33
+ | Promise<any>
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Get auth data from request - tries session cookies first, then JWT header
39
+ * Session: web apps with cookies forwarded by zero
40
+ * JWT: native apps (Tauri, React Native) using Authorization header
41
+ */
42
+ export async function getAuthDataFromRequest(
43
+ authServer: AuthServer,
44
+ req: Request,
45
+ tokenOptions?: ValidateTokenOptions
46
+ ): Promise<AuthData | null> {
47
+ // from react native, better auth doesnt send cookie but insteead only the Authorization
48
+ // but better auth wants to find the cookie here, so re-route it:
49
+
50
+ const authHeader = req.headers.get('authorization')
51
+ const cookie = authHeader?.split('Bearer ')[1]
52
+
53
+ const newHeaders = new Headers(req.headers)
54
+ if (cookie) {
55
+ newHeaders.set('Cookie', cookie)
56
+ }
57
+
58
+ // try session-based auth first (web - cookies forwarded by zero)
59
+ try {
60
+ const session = await authServer.api.getSession({ headers: newHeaders })
61
+ if (session?.user) {
62
+ return {
63
+ id: session.user.id,
64
+ email: session.user.email || undefined,
65
+ role: session.user.role === 'admin' ? 'admin' : undefined,
66
+ }
67
+ }
68
+ } catch {
69
+ // session auth failed, try JWT
70
+ }
71
+
72
+ // try authorization header (token-based auth for native/tauri)
73
+
74
+ const jwtToken = authHeader?.replace('Bearer ', '')
75
+
76
+ if (jwtToken) {
77
+ try {
78
+ const payload = await validateToken(jwtToken, tokenOptions)
79
+ const userId = (payload as any)?.id || payload?.sub
80
+ if (userId) {
81
+ return {
82
+ id: userId as string,
83
+ email: (payload as any).email as string | undefined,
84
+ role: (payload as any).role === 'admin' ? 'admin' : undefined,
85
+ }
86
+ }
87
+ } catch (err) {
88
+ if (!(err instanceof InvalidTokenError)) {
89
+ throw err
90
+ }
91
+ }
92
+ }
93
+
94
+ return null
95
+ }
96
+
97
+ // jwt validation for native apps
98
+
99
+ export async function validateToken(
100
+ token: string,
101
+ options?: ValidateTokenOptions
102
+ ): Promise<JWTPayload> {
103
+ const {
104
+ baseUrl = process.env.ONE_SERVER_URL,
105
+ forceIssuer = process.env.FORCE_ISSUER || '',
106
+ jwksPath = '/api/auth/jwks',
107
+ } = options || {}
108
+
109
+ if (!baseUrl) {
110
+ throw new Error(`No baseURL!`)
111
+ }
112
+
113
+ const normalizedBaseUrl = removeTrailingSlash(baseUrl)
114
+ const url = `${forceIssuer || normalizedBaseUrl}${jwksPath}`
115
+
116
+ // create fresh JWKS fetcher each time to avoid stale key cache issues
117
+ const JWKS = createRemoteJWKSet(new URL(url))
118
+
119
+ try {
120
+ const verifyOptions = forceIssuer
121
+ ? {}
122
+ : {
123
+ issuer: normalizedBaseUrl,
124
+ audience: normalizedBaseUrl,
125
+ }
126
+
127
+ const { payload } = await jwtVerify(token, JWKS, verifyOptions)
128
+
129
+ return payload
130
+ } catch (error) {
131
+ throw new InvalidTokenError(`${error}`)
132
+ }
133
+ }
134
+
135
+ export async function isValidJWT(
136
+ token: string,
137
+ options: ValidateTokenOptions
138
+ ): Promise<boolean> {
139
+ try {
140
+ await validateToken(token, options)
141
+ return true
142
+ } catch {
143
+ return false
144
+ }
145
+ }
146
+
147
+ function removeTrailingSlash(str: string) {
148
+ return str.replace(/\/$/, '')
149
+ }