@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.
- package/README.md +74 -104
- package/dist/cjs/createAuthClient.cjs +38 -27
- package/dist/cjs/createAuthClient.js +32 -37
- package/dist/cjs/createAuthClient.js.map +2 -2
- package/dist/cjs/createAuthClient.native.js +57 -33
- package/dist/cjs/createAuthClient.native.js.map +1 -1
- package/dist/cjs/server.cjs +94 -0
- package/dist/cjs/server.js +87 -0
- package/dist/cjs/server.js.map +6 -0
- package/dist/cjs/server.native.js +196 -0
- package/dist/cjs/server.native.js.map +1 -0
- package/dist/esm/createAuthClient.js +32 -37
- package/dist/esm/createAuthClient.js.map +2 -2
- package/dist/esm/createAuthClient.mjs +38 -27
- package/dist/esm/createAuthClient.mjs.map +1 -1
- package/dist/esm/createAuthClient.native.js +57 -33
- package/dist/esm/createAuthClient.native.js.map +1 -1
- package/dist/esm/server.js +71 -0
- package/dist/esm/server.js.map +6 -0
- package/dist/esm/server.mjs +67 -0
- package/dist/esm/server.mjs.map +1 -0
- package/dist/esm/server.native.js +166 -0
- package/dist/esm/server.native.js.map +1 -0
- package/package.json +11 -4
- package/src/createAuthClient.ts +74 -70
- package/src/server.ts +149 -0
- package/types/createAuthClient.d.ts +13 -6
- package/types/createAuthClient.d.ts.map +2 -2
- package/types/server.d.ts +45 -0
- package/types/server.d.ts.map +19 -0
|
@@ -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":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@take-out/better-auth-utils",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
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-
|
|
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",
|
package/src/createAuthClient.ts
CHANGED
|
@@ -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
|
-
*
|
|
64
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
+
}
|