@fluixi/auth 1.0.0-alpha.100
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 +72 -0
- package/dist/client.cjs +219 -0
- package/dist/client.d.ts +177 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +291 -0
- package/dist/client.mjs +199 -0
- package/dist/cookies.cjs +43 -0
- package/dist/cookies.d.ts +7 -0
- package/dist/cookies.d.ts.map +1 -0
- package/dist/cookies.js +19 -0
- package/dist/cookies.mjs +22 -0
- package/dist/remote.cjs +52 -0
- package/dist/remote.d.ts +20 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +30 -0
- package/dist/remote.mjs +31 -0
- package/dist/server.cjs +203 -0
- package/dist/server.d.ts +131 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +167 -0
- package/dist/server.mjs +183 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/types.cjs +26 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.mjs +5 -0
- package/package.json +76 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fluixi/auth/client: the reactive client hooks.
|
|
3
|
+
*
|
|
4
|
+
* The session is an isomorphic store: resolved on the server from `locals.auth`
|
|
5
|
+
* (serialized into the page) and seeded synchronously on the client → no auth flash, no
|
|
6
|
+
* post-hydration refetch. Two ways to drive it:
|
|
7
|
+
* • `{ client }`: a Better Auth client.
|
|
8
|
+
* • `{ session }`: an existing reactive session (e.g. @fluixi/session's createSession or
|
|
9
|
+
* any `{ user(), login?, logout? }`). The framework OWNS the reactive sync, so apps stop
|
|
10
|
+
* hand-writing a `createEffect` to mirror their store into the auth hooks.
|
|
11
|
+
*
|
|
12
|
+
* Client-only deps: fluixi + @fluixi/dom's pure `getLocals`, never @fluixi/server.
|
|
13
|
+
*/
|
|
14
|
+
import { createIsomorphicStore, createEffect, createRoot, env } from '@fluixi/core';
|
|
15
|
+
import { getLocals } from '@fluixi/dom';
|
|
16
|
+
import { AUTH_LOCALS_KEY, } from './types.js';
|
|
17
|
+
export function createAuthHooks(options) {
|
|
18
|
+
const sess = options.session;
|
|
19
|
+
const store = createIsomorphicStore('auth', {
|
|
20
|
+
// Server: read what authMiddleware put on locals → serialized for the client to seed.
|
|
21
|
+
server: () => getLocals()[AUTH_LOCALS_KEY] ?? { user: null, session: null },
|
|
22
|
+
// SPA cold load: from the provided session, else the Better Auth client.
|
|
23
|
+
client: async () => sess
|
|
24
|
+
? { user: sess.user(), session: null }
|
|
25
|
+
: normalize(await options.client.getSession()),
|
|
26
|
+
fallback: { user: null, session: null },
|
|
27
|
+
});
|
|
28
|
+
// `{ session }` path: the framework owns the reactive bridge (no app-level effect).
|
|
29
|
+
if (sess) {
|
|
30
|
+
const apply = () => {
|
|
31
|
+
if (options.ready && !options.ready())
|
|
32
|
+
return;
|
|
33
|
+
store.set({ user: sess.user(), session: null });
|
|
34
|
+
};
|
|
35
|
+
apply(); // immediate, so a first protectedRoute read isn't stale
|
|
36
|
+
createRoot(() => createEffect(apply, undefined, { name: 'auth ← session' }));
|
|
37
|
+
}
|
|
38
|
+
const refresh = async () => {
|
|
39
|
+
store.set(sess ? { user: sess.user(), session: null } : normalize(await options.client.getSession()));
|
|
40
|
+
};
|
|
41
|
+
const signIn = sess
|
|
42
|
+
? ((...args) => sess.login?.(...args) ?? Promise.resolve())
|
|
43
|
+
: async (creds) => {
|
|
44
|
+
await options.client.signIn.email(creds);
|
|
45
|
+
await refresh();
|
|
46
|
+
};
|
|
47
|
+
const signOut = sess
|
|
48
|
+
? () => sess.logout?.() ?? Promise.resolve()
|
|
49
|
+
: async () => {
|
|
50
|
+
await options.client.signOut();
|
|
51
|
+
store.set({ user: null, session: null });
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Send the visitor somewhere else.
|
|
55
|
+
*
|
|
56
|
+
* The route's own `navigator` wins, then the one given to `createAuthHooks`, then a
|
|
57
|
+
* document load. A handler is a factory rather than a function: `useNavigate` has to
|
|
58
|
+
* run inside the component being rendered, so it is called here, at the redirect,
|
|
59
|
+
* instead of when the hooks were built.
|
|
60
|
+
*
|
|
61
|
+
* Without a handler this assigns `location`, which loads a document and rebuilds the
|
|
62
|
+
* whole app. That is right for an app with no client router and wrong for one that
|
|
63
|
+
* has it, which is what the option exists to fix.
|
|
64
|
+
*/
|
|
65
|
+
const go = (to, opts) => {
|
|
66
|
+
const fromRoute = typeof opts?.handler === 'function' ? opts.handler() : undefined;
|
|
67
|
+
const fromHooks = typeof options.navigate === 'function'
|
|
68
|
+
? options.navigate
|
|
69
|
+
: typeof options.navigate?.handler === 'function'
|
|
70
|
+
? // Called, not passed along. Handing back the factory made `fn(to)` build a
|
|
71
|
+
// navigate function and return it, so the redirect silently did nothing.
|
|
72
|
+
options.navigate.handler()
|
|
73
|
+
: undefined;
|
|
74
|
+
const fn = fromRoute ?? fromHooks;
|
|
75
|
+
if (fn) {
|
|
76
|
+
const navigateOptions = opts?.options ??
|
|
77
|
+
(typeof options.navigate === 'object' ? options.navigate?.options : undefined);
|
|
78
|
+
// Only when there are some. Deciding on the handler's arity instead dropped them
|
|
79
|
+
// for anything declaring the parameter optional, which `useNavigate` does; passing
|
|
80
|
+
// `undefined` regardless would change the call every handler sees.
|
|
81
|
+
if (navigateOptions)
|
|
82
|
+
fn(to, navigateOptions);
|
|
83
|
+
else
|
|
84
|
+
fn(to);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
env.window?.location.assign(to);
|
|
88
|
+
};
|
|
89
|
+
// ── RBAC surface (driven by the bound session's access config) ────────────────
|
|
90
|
+
const permissions = () => sess?.permissions?.();
|
|
91
|
+
const can = (query) => sess?.can?.(query) ?? false;
|
|
92
|
+
const roleList = () => {
|
|
93
|
+
if (sess?.roles)
|
|
94
|
+
return sess.roles();
|
|
95
|
+
const r = store.fields.user()?.role;
|
|
96
|
+
return r ? [r] : [];
|
|
97
|
+
};
|
|
98
|
+
const passesRole = (opts) => {
|
|
99
|
+
if (opts?.role && !roleList().includes(opts.role))
|
|
100
|
+
return false;
|
|
101
|
+
if (opts?.roles && !opts.roles.some((r) => roleList().includes(r)))
|
|
102
|
+
return false;
|
|
103
|
+
if (opts?.permission !== undefined && !can(opts.permission))
|
|
104
|
+
return false;
|
|
105
|
+
return true;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Show something only when the access matcher allows it.
|
|
109
|
+
*
|
|
110
|
+
* Asks a permission question, which is the finer of the two: `Protect` asks who
|
|
111
|
+
* someone is, this asks what they may do. Reactive, so it follows a role change
|
|
112
|
+
* without a reload.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```tsx
|
|
116
|
+
* <Can perform={(p) => p.invoices.refund} fallback={<Tooltip>Ask an admin</Tooltip>}>
|
|
117
|
+
* <button onClick={refund}>Refund</button>
|
|
118
|
+
* </Can>
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
function Can(props) {
|
|
122
|
+
return () => (can(props.perform) ? props.children : (props.fallback ?? null));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Show a section of a page to some people and not others.
|
|
126
|
+
*
|
|
127
|
+
* Renders `fallback` for a signed-out visitor, and for a signed-in one who lacks the
|
|
128
|
+
* role. Nothing navigates: the rest of the page stays exactly as it is, which is the
|
|
129
|
+
* difference from `protectedRoute`.
|
|
130
|
+
*
|
|
131
|
+
* Reach for it inside a page. Reach for `protectedRoute` when the whole page should
|
|
132
|
+
* not have been reachable.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```tsx
|
|
136
|
+
* <Protect roles={['admin', 'support']} fallback={null}>
|
|
137
|
+
* <DangerZone />
|
|
138
|
+
* </Protect>
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
function Protect(props) {
|
|
142
|
+
return () => {
|
|
143
|
+
if (!store.fields.user())
|
|
144
|
+
return props.fallback ?? null;
|
|
145
|
+
return passesRole(props) ? props.children : (props.fallback ?? null);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Wrap a page so the wrong visitor never reaches it.
|
|
150
|
+
*
|
|
151
|
+
* Unlike `Protect`, this **navigates**: a signed-out visitor goes to `loginPath`, one
|
|
152
|
+
* lacking the role goes to `forbiddenPath`, and with `guestOnly` a signed-in visitor
|
|
153
|
+
* goes to `homePath`. The component never runs, so a page that loads data on mount
|
|
154
|
+
* does not load it for someone who should not see it.
|
|
155
|
+
*
|
|
156
|
+
* This is a client-side guard and a rendering decision. It is not authorization: the
|
|
157
|
+
* server has to refuse the request as well, because a client can be made to skip this.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```tsx
|
|
161
|
+
* // the whole page, behind a role
|
|
162
|
+
* export default protectedRoute(AdminPage, { role: 'admin' });
|
|
163
|
+
*
|
|
164
|
+
* // a login page, which a signed-in visitor has no use for
|
|
165
|
+
* export default protectedRoute(LoginPage, { guestOnly: true });
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
function protectedRoute(Comp, opts) {
|
|
169
|
+
return (props) => {
|
|
170
|
+
const user = store.fields.user();
|
|
171
|
+
if (opts?.guestOnly) {
|
|
172
|
+
if (user)
|
|
173
|
+
return go(options.homePath ?? '/', opts?.navigator), null;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
if (!user)
|
|
177
|
+
return go(options.loginPath ?? '/login', opts?.navigator), null;
|
|
178
|
+
if (!passesRole(opts))
|
|
179
|
+
return go(options.forbiddenPath ?? '/403'), null;
|
|
180
|
+
}
|
|
181
|
+
return Comp(props);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
/**
|
|
186
|
+
* The signed-in user, or null.
|
|
187
|
+
*
|
|
188
|
+
* Returns the accessor rather than the value, so a component reads it where it
|
|
189
|
+
* renders and re-renders when it changes.
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```tsx
|
|
193
|
+
* const user = useUser();
|
|
194
|
+
* <span>{() => user()?.email ?? 'Signed out'}</span>
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
useUser: () => store.fields.user,
|
|
198
|
+
/**
|
|
199
|
+
* The session record beside the user: when it expires, and whatever else the
|
|
200
|
+
* backend attached. Null when signed out.
|
|
201
|
+
*/
|
|
202
|
+
useSession: () => store.fields.session,
|
|
203
|
+
/**
|
|
204
|
+
* Everything at once, for a component that needs more than one of them.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```tsx
|
|
208
|
+
* const { user, isAuthenticated, can, signOut } = useAuth();
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
useAuth: () => ({
|
|
212
|
+
user: store.fields.user,
|
|
213
|
+
session: store.fields.session,
|
|
214
|
+
isAuthenticated: () => store.fields.user() !== null,
|
|
215
|
+
roles: roleList,
|
|
216
|
+
can,
|
|
217
|
+
signIn,
|
|
218
|
+
signOut,
|
|
219
|
+
}),
|
|
220
|
+
/**
|
|
221
|
+
* Start a sign-in.
|
|
222
|
+
*
|
|
223
|
+
* What it takes depends on what the hooks were given: credentials for a Better Auth
|
|
224
|
+
* client, and for a redirect flow whatever that session's `login` expects.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```tsx
|
|
228
|
+
* const signIn = useSignIn();
|
|
229
|
+
* <button onClick={() => signIn({ email, password })}>Sign in</button>
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
useSignIn: () => signIn,
|
|
233
|
+
/**
|
|
234
|
+
* End the session.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```tsx
|
|
238
|
+
* const signOut = useSignOut();
|
|
239
|
+
* <button onClick={() => signOut()}>Sign out</button>
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
useSignOut: () => signOut,
|
|
243
|
+
/**
|
|
244
|
+
* The permission set for the current roles, reactively.
|
|
245
|
+
*
|
|
246
|
+
* A rendering hint and nothing more: a client can claim any permission, so the
|
|
247
|
+
* server decides what is allowed on every request.
|
|
248
|
+
*/
|
|
249
|
+
usePermissions: () => permissions,
|
|
250
|
+
/**
|
|
251
|
+
* Ask whether the current user may do something.
|
|
252
|
+
*
|
|
253
|
+
* `false` when the session supplies no RBAC, so a missing `access` config hides a
|
|
254
|
+
* control rather than revealing it.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```tsx
|
|
258
|
+
* const can = useCan();
|
|
259
|
+
* <button disabled={() => !can((p) => p.users.delete)}>Delete</button>
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
useCan: () => can,
|
|
263
|
+
Can,
|
|
264
|
+
Protect,
|
|
265
|
+
/**
|
|
266
|
+
* Re-read the session from the backend.
|
|
267
|
+
*
|
|
268
|
+
* For after something changed outside this tab: a role granted elsewhere, or a
|
|
269
|
+
* return from a flow that altered the account.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* await refresh();
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
refresh,
|
|
277
|
+
protectedRoute,
|
|
278
|
+
/**
|
|
279
|
+
* The underlying isomorphic store.
|
|
280
|
+
*
|
|
281
|
+
* Escape hatch. It is here for a test that wants to seed a user, and for the rare
|
|
282
|
+
* caller that needs to write what the hooks read. Prefer the accessors above:
|
|
283
|
+
* writing to the store directly puts the interface out of step with the session
|
|
284
|
+
* that owns it.
|
|
285
|
+
*/
|
|
286
|
+
store,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function normalize(r) {
|
|
290
|
+
return { user: r.data?.user ?? null, session: r.data?.session ?? null };
|
|
291
|
+
}
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/*! @fluixi/auth v1.0.0-alpha.100 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
|
|
3
|
+
// src/client.ts
|
|
4
|
+
import { createIsomorphicStore, createEffect, createRoot, env } from "@fluixi/core";
|
|
5
|
+
import { getLocals } from "@fluixi/dom";
|
|
6
|
+
|
|
7
|
+
// src/types.ts
|
|
8
|
+
var AUTH_LOCALS_KEY = "auth";
|
|
9
|
+
|
|
10
|
+
// src/client.ts
|
|
11
|
+
function createAuthHooks(options) {
|
|
12
|
+
const sess = options.session;
|
|
13
|
+
const store = createIsomorphicStore("auth", {
|
|
14
|
+
// Server: read what authMiddleware put on locals → serialized for the client to seed.
|
|
15
|
+
server: () => getLocals()[AUTH_LOCALS_KEY] ?? { user: null, session: null },
|
|
16
|
+
// SPA cold load: from the provided session, else the Better Auth client.
|
|
17
|
+
client: async () => sess ? { user: sess.user(), session: null } : normalize(await options.client.getSession()),
|
|
18
|
+
fallback: { user: null, session: null }
|
|
19
|
+
});
|
|
20
|
+
if (sess) {
|
|
21
|
+
const apply = () => {
|
|
22
|
+
if (options.ready && !options.ready()) return;
|
|
23
|
+
store.set({ user: sess.user(), session: null });
|
|
24
|
+
};
|
|
25
|
+
apply();
|
|
26
|
+
createRoot(() => createEffect(apply, void 0, { name: "auth ← session" }));
|
|
27
|
+
}
|
|
28
|
+
const refresh = async () => {
|
|
29
|
+
store.set(sess ? { user: sess.user(), session: null } : normalize(await options.client.getSession()));
|
|
30
|
+
};
|
|
31
|
+
const signIn = sess ? ((...args) => sess.login?.(...args) ?? Promise.resolve()) : async (creds) => {
|
|
32
|
+
await options.client.signIn.email(creds);
|
|
33
|
+
await refresh();
|
|
34
|
+
};
|
|
35
|
+
const signOut = sess ? () => sess.logout?.() ?? Promise.resolve() : async () => {
|
|
36
|
+
await options.client.signOut();
|
|
37
|
+
store.set({ user: null, session: null });
|
|
38
|
+
};
|
|
39
|
+
const go = (to, opts) => {
|
|
40
|
+
const fromRoute = typeof opts?.handler === "function" ? opts.handler() : void 0;
|
|
41
|
+
const fromHooks = typeof options.navigate === "function" ? options.navigate : typeof options.navigate?.handler === "function" ? (
|
|
42
|
+
// Called, not passed along. Handing back the factory made `fn(to)` build a
|
|
43
|
+
// navigate function and return it, so the redirect silently did nothing.
|
|
44
|
+
options.navigate.handler()
|
|
45
|
+
) : void 0;
|
|
46
|
+
const fn = fromRoute ?? fromHooks;
|
|
47
|
+
if (fn) {
|
|
48
|
+
const navigateOptions = opts?.options ?? (typeof options.navigate === "object" ? options.navigate?.options : void 0);
|
|
49
|
+
if (navigateOptions) fn(to, navigateOptions);
|
|
50
|
+
else fn(to);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
env.window?.location.assign(to);
|
|
54
|
+
};
|
|
55
|
+
const permissions = () => sess?.permissions?.();
|
|
56
|
+
const can = (query) => sess?.can?.(query) ?? false;
|
|
57
|
+
const roleList = () => {
|
|
58
|
+
if (sess?.roles) return sess.roles();
|
|
59
|
+
const r = store.fields.user()?.role;
|
|
60
|
+
return r ? [r] : [];
|
|
61
|
+
};
|
|
62
|
+
const passesRole = (opts) => {
|
|
63
|
+
if (opts?.role && !roleList().includes(opts.role)) return false;
|
|
64
|
+
if (opts?.roles && !opts.roles.some((r) => roleList().includes(r))) return false;
|
|
65
|
+
if (opts?.permission !== void 0 && !can(opts.permission)) return false;
|
|
66
|
+
return true;
|
|
67
|
+
};
|
|
68
|
+
function Can(props) {
|
|
69
|
+
return () => can(props.perform) ? props.children : props.fallback ?? null;
|
|
70
|
+
}
|
|
71
|
+
function Protect(props) {
|
|
72
|
+
return () => {
|
|
73
|
+
if (!store.fields.user()) return props.fallback ?? null;
|
|
74
|
+
return passesRole(props) ? props.children : props.fallback ?? null;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function protectedRoute(Comp, opts) {
|
|
78
|
+
return (props) => {
|
|
79
|
+
const user = store.fields.user();
|
|
80
|
+
if (opts?.guestOnly) {
|
|
81
|
+
if (user) return go(options.homePath ?? "/", opts?.navigator), null;
|
|
82
|
+
} else {
|
|
83
|
+
if (!user) return go(options.loginPath ?? "/login", opts?.navigator), null;
|
|
84
|
+
if (!passesRole(opts)) return go(options.forbiddenPath ?? "/403"), null;
|
|
85
|
+
}
|
|
86
|
+
return Comp(props);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
/**
|
|
91
|
+
* The signed-in user, or null.
|
|
92
|
+
*
|
|
93
|
+
* Returns the accessor rather than the value, so a component reads it where it
|
|
94
|
+
* renders and re-renders when it changes.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```tsx
|
|
98
|
+
* const user = useUser();
|
|
99
|
+
* <span>{() => user()?.email ?? 'Signed out'}</span>
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
useUser: () => store.fields.user,
|
|
103
|
+
/**
|
|
104
|
+
* The session record beside the user: when it expires, and whatever else the
|
|
105
|
+
* backend attached. Null when signed out.
|
|
106
|
+
*/
|
|
107
|
+
useSession: () => store.fields.session,
|
|
108
|
+
/**
|
|
109
|
+
* Everything at once, for a component that needs more than one of them.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```tsx
|
|
113
|
+
* const { user, isAuthenticated, can, signOut } = useAuth();
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
useAuth: () => ({
|
|
117
|
+
user: store.fields.user,
|
|
118
|
+
session: store.fields.session,
|
|
119
|
+
isAuthenticated: () => store.fields.user() !== null,
|
|
120
|
+
roles: roleList,
|
|
121
|
+
can,
|
|
122
|
+
signIn,
|
|
123
|
+
signOut
|
|
124
|
+
}),
|
|
125
|
+
/**
|
|
126
|
+
* Start a sign-in.
|
|
127
|
+
*
|
|
128
|
+
* What it takes depends on what the hooks were given: credentials for a Better Auth
|
|
129
|
+
* client, and for a redirect flow whatever that session's `login` expects.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```tsx
|
|
133
|
+
* const signIn = useSignIn();
|
|
134
|
+
* <button onClick={() => signIn({ email, password })}>Sign in</button>
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
useSignIn: () => signIn,
|
|
138
|
+
/**
|
|
139
|
+
* End the session.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```tsx
|
|
143
|
+
* const signOut = useSignOut();
|
|
144
|
+
* <button onClick={() => signOut()}>Sign out</button>
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
useSignOut: () => signOut,
|
|
148
|
+
/**
|
|
149
|
+
* The permission set for the current roles, reactively.
|
|
150
|
+
*
|
|
151
|
+
* A rendering hint and nothing more: a client can claim any permission, so the
|
|
152
|
+
* server decides what is allowed on every request.
|
|
153
|
+
*/
|
|
154
|
+
usePermissions: () => permissions,
|
|
155
|
+
/**
|
|
156
|
+
* Ask whether the current user may do something.
|
|
157
|
+
*
|
|
158
|
+
* `false` when the session supplies no RBAC, so a missing `access` config hides a
|
|
159
|
+
* control rather than revealing it.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```tsx
|
|
163
|
+
* const can = useCan();
|
|
164
|
+
* <button disabled={() => !can((p) => p.users.delete)}>Delete</button>
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
useCan: () => can,
|
|
168
|
+
Can,
|
|
169
|
+
Protect,
|
|
170
|
+
/**
|
|
171
|
+
* Re-read the session from the backend.
|
|
172
|
+
*
|
|
173
|
+
* For after something changed outside this tab: a role granted elsewhere, or a
|
|
174
|
+
* return from a flow that altered the account.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* await refresh();
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
refresh,
|
|
182
|
+
protectedRoute,
|
|
183
|
+
/**
|
|
184
|
+
* The underlying isomorphic store.
|
|
185
|
+
*
|
|
186
|
+
* Escape hatch. It is here for a test that wants to seed a user, and for the rare
|
|
187
|
+
* caller that needs to write what the hooks read. Prefer the accessors above:
|
|
188
|
+
* writing to the store directly puts the interface out of step with the session
|
|
189
|
+
* that owns it.
|
|
190
|
+
*/
|
|
191
|
+
store
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function normalize(r) {
|
|
195
|
+
return { user: r.data?.user ?? null, session: r.data?.session ?? null };
|
|
196
|
+
}
|
|
197
|
+
export {
|
|
198
|
+
createAuthHooks
|
|
199
|
+
};
|
package/dist/cookies.cjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/cookies.ts
|
|
21
|
+
var cookies_exports = {};
|
|
22
|
+
__export(cookies_exports, {
|
|
23
|
+
deleteCookie: () => import_cookie2.deleteCookie,
|
|
24
|
+
getCookie: () => getCookie,
|
|
25
|
+
getCookies: () => getCookies,
|
|
26
|
+
isValidCookieName: () => import_cookie2.isValidCookieName,
|
|
27
|
+
parseCookies: () => import_cookie2.parseCookies,
|
|
28
|
+
serializeCookie: () => import_cookie2.serializeCookie
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(cookies_exports);
|
|
31
|
+
var import_server = require("@fluixi/server");
|
|
32
|
+
var import_cookie = require("@fluixi/utils/cookie");
|
|
33
|
+
var import_cookie2 = require("@fluixi/utils/cookie");
|
|
34
|
+
function requestHeaders() {
|
|
35
|
+
const req = (0, import_server.getRequestEvent)()?.request;
|
|
36
|
+
return req?.headers ?? null;
|
|
37
|
+
}
|
|
38
|
+
function getCookie(name) {
|
|
39
|
+
return (0, import_cookie.parseCookies)(requestHeaders()?.get("cookie"))[name] ?? null;
|
|
40
|
+
}
|
|
41
|
+
function getCookies() {
|
|
42
|
+
return (0, import_cookie.parseCookies)(requestHeaders()?.get("cookie"));
|
|
43
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { parseCookies, serializeCookie, deleteCookie, isValidCookieName } from '@fluixi/utils/cookie';
|
|
2
|
+
export type { CookieOptions } from '@fluixi/utils/cookie';
|
|
3
|
+
/** Read one cookie from the CURRENT request (request-scoped). */
|
|
4
|
+
export declare function getCookie(name: string): string | null;
|
|
5
|
+
/** All cookies on the current request. */
|
|
6
|
+
export declare function getCookies(): Record<string, string>;
|
|
7
|
+
//# sourceMappingURL=cookies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../src/cookies.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACtG,YAAY,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAO1D,iEAAiE;AACjE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAErD;AAED,0CAA0C;AAC1C,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEnD"}
|
package/dist/cookies.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-scoped cookie reads. The codec lives in `@fluixi/utils/cookie` and is re-exported
|
|
3
|
+
* here; middleware appends `serializeCookie` output to its Response's `set-cookie` header.
|
|
4
|
+
*/
|
|
5
|
+
import { getRequestEvent } from '@fluixi/server';
|
|
6
|
+
import { parseCookies } from '@fluixi/utils/cookie';
|
|
7
|
+
export { parseCookies, serializeCookie, deleteCookie, isValidCookieName } from '@fluixi/utils/cookie';
|
|
8
|
+
function requestHeaders() {
|
|
9
|
+
const req = getRequestEvent()?.request;
|
|
10
|
+
return req?.headers ?? null;
|
|
11
|
+
}
|
|
12
|
+
/** Read one cookie from the CURRENT request (request-scoped). */
|
|
13
|
+
export function getCookie(name) {
|
|
14
|
+
return parseCookies(requestHeaders()?.get('cookie'))[name] ?? null;
|
|
15
|
+
}
|
|
16
|
+
/** All cookies on the current request. */
|
|
17
|
+
export function getCookies() {
|
|
18
|
+
return parseCookies(requestHeaders()?.get('cookie'));
|
|
19
|
+
}
|
package/dist/cookies.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/cookies.ts
|
|
2
|
+
import { getRequestEvent } from "@fluixi/server";
|
|
3
|
+
import { parseCookies } from "@fluixi/utils/cookie";
|
|
4
|
+
import { parseCookies as parseCookies2, serializeCookie, deleteCookie, isValidCookieName } from "@fluixi/utils/cookie";
|
|
5
|
+
function requestHeaders() {
|
|
6
|
+
const req = getRequestEvent()?.request;
|
|
7
|
+
return req?.headers ?? null;
|
|
8
|
+
}
|
|
9
|
+
function getCookie(name) {
|
|
10
|
+
return parseCookies(requestHeaders()?.get("cookie"))[name] ?? null;
|
|
11
|
+
}
|
|
12
|
+
function getCookies() {
|
|
13
|
+
return parseCookies(requestHeaders()?.get("cookie"));
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
deleteCookie,
|
|
17
|
+
getCookie,
|
|
18
|
+
getCookies,
|
|
19
|
+
isValidCookieName,
|
|
20
|
+
parseCookies2 as parseCookies,
|
|
21
|
+
serializeCookie
|
|
22
|
+
};
|
package/dist/remote.cjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/remote.ts
|
|
21
|
+
var remote_exports = {};
|
|
22
|
+
__export(remote_exports, {
|
|
23
|
+
remoteAuth: () => remoteAuth
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(remote_exports);
|
|
26
|
+
function remoteAuth(options) {
|
|
27
|
+
const base = new URL((options.basePath ?? "/api/auth").replace(/\/$/, "") + "/", options.url);
|
|
28
|
+
const call = (input, init) => (options.fetch ?? globalThis.fetch)(input, init);
|
|
29
|
+
return {
|
|
30
|
+
async handler(request) {
|
|
31
|
+
const url = new URL(request.url);
|
|
32
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
33
|
+
return call(new URL(url.pathname + url.search, base), {
|
|
34
|
+
method: request.method,
|
|
35
|
+
headers: request.headers,
|
|
36
|
+
body: hasBody ? request.body : void 0,
|
|
37
|
+
redirect: "manual",
|
|
38
|
+
...hasBody ? { duplex: "half" } : {}
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
api: {
|
|
42
|
+
async getSession({ headers }) {
|
|
43
|
+
const cookie = headers.get("cookie");
|
|
44
|
+
if (!cookie) return null;
|
|
45
|
+
const response = await call(new URL("get-session", base), { headers: { cookie, accept: "application/json" } });
|
|
46
|
+
if (!response.ok) return null;
|
|
47
|
+
const body = await response.json().catch(() => null);
|
|
48
|
+
return body && body.user ? { user: body.user, session: body.session ?? null } : null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
package/dist/remote.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An auth engine that lives in another service. A web app whose Better Auth runs in its API
|
|
3
|
+
* registers this with `defineAuth(remoteAuth({ url }))`; `authMiddleware()`, `requireUser()` and
|
|
4
|
+
* the client hooks then work as they would with the engine in-process, each session read being
|
|
5
|
+
* one request to the API with the visitor's cookie.
|
|
6
|
+
*
|
|
7
|
+
* The API's guards (`member()`, `hasPermission`) call the engine directly and stay there: a web
|
|
8
|
+
* app reads the session, the API decides what it may do.
|
|
9
|
+
*/
|
|
10
|
+
import type { BetterAuthServer } from './types.js';
|
|
11
|
+
export interface RemoteAuthOptions {
|
|
12
|
+
/** The API's origin, `http://localhost:3301`. */
|
|
13
|
+
url: string;
|
|
14
|
+
/** Where the API mounts its auth routes. `/api/auth` by default. */
|
|
15
|
+
basePath?: string;
|
|
16
|
+
/** `globalThis.fetch` by default. */
|
|
17
|
+
fetch?: typeof fetch;
|
|
18
|
+
}
|
|
19
|
+
export declare function remoteAuth(options: RemoteAuthOptions): BetterAuthServer;
|
|
20
|
+
//# sourceMappingURL=remote.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.d.ts","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CA4BvE"}
|