@douvery/auth 0.3.2 → 0.3.3

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.
@@ -1,5 +1,10 @@
1
1
  /**
2
2
  * @douvery/auth/qwik - Qwik adapter
3
+ *
4
+ * Uses QRL for config to avoid Qwik serialization issues with
5
+ * function-based storage adapters (customStorage).
6
+ * The client is created inside useVisibleTask$ and wrapped with
7
+ * noSerialize() since DouveryAuthClient has non-serializable methods.
3
8
  */
4
9
 
5
10
  import {
@@ -10,8 +15,12 @@ import {
10
15
  useTask$,
11
16
  useVisibleTask$,
12
17
  component$,
18
+ $,
13
19
  Slot,
20
+ noSerialize,
14
21
  type Signal,
22
+ type NoSerialize,
23
+ type QRL,
15
24
  } from "@builder.io/qwik";
16
25
  import {
17
26
  DouveryAuthClient,
@@ -33,47 +42,96 @@ import {
33
42
  type AuthUrl,
34
43
  } from "@douvery/auth";
35
44
 
45
+ // ============================================================================
46
+ // Context
47
+ // ============================================================================
48
+
36
49
  interface DouveryAuthContextValue {
37
50
  state: Signal<AuthState>;
38
51
  isInitialized: Signal<boolean>;
39
52
  isLoading: Signal<boolean>;
40
53
  error: Signal<Error | null>;
41
- client: DouveryAuthClient;
54
+ clientRef: Signal<NoSerialize<DouveryAuthClient> | undefined>;
55
+ /** Application-specific user data from SSR (e.g. routeLoader$). */
56
+ appUser: Signal<unknown>;
57
+ appUserAuthenticated: Signal<boolean>;
42
58
  }
43
59
 
44
60
  export const DouveryAuthContext =
45
61
  createContextId<DouveryAuthContextValue>("douvery-auth");
46
62
 
63
+ // ============================================================================
64
+ // Provider
65
+ // ============================================================================
66
+
47
67
  export interface DouveryAuthProviderProps {
48
- config: DouveryAuthConfig;
68
+ /**
69
+ * QRL that returns the auth configuration.
70
+ * Use $(() => getDouveryAuthConfig()) to wrap your config factory.
71
+ * This avoids Qwik serialization issues with customStorage functions.
72
+ */
73
+ config$: QRL<() => DouveryAuthConfig>;
74
+ /**
75
+ * Optional application-specific user data loaded from SSR (routeLoader$).
76
+ * This is separate from OAuth user – it holds the full app user object
77
+ * (e.g. UserACC with address, active, sessionId, etc.).
78
+ * Pass the routeLoader$ signal directly.
79
+ */
80
+ appUser?: Signal<unknown>;
49
81
  }
50
82
 
83
+ const DEFAULT_STATE: AuthState = {
84
+ status: "unauthenticated",
85
+ user: null,
86
+ tokens: null,
87
+ error: null,
88
+ };
89
+
51
90
  export const DouveryAuthProvider = component$<DouveryAuthProviderProps>(
52
- ({ config }) => {
53
- const client = createDouveryAuth(config);
54
- const state = useSignal<AuthState>(client.getState());
91
+ ({ config$, appUser: externalAppUser }) => {
92
+ // All signals are serializable - no functions stored directly
93
+ const state = useSignal<AuthState>(DEFAULT_STATE);
55
94
  const isInitialized = useSignal(false);
56
95
  const isLoading = useSignal(false);
57
96
  const error = useSignal<Error | null>(null);
97
+ const clientRef = useSignal<NoSerialize<DouveryAuthClient>>();
98
+
99
+ // App user data: use external signal if provided, otherwise create internal one
100
+ const internalAppUser = useSignal<unknown>(externalAppUser?.value ?? null);
101
+ const appUser = externalAppUser ?? internalAppUser;
102
+ const appUserAuthenticated = useSignal<boolean>(!!appUser.value);
103
+
104
+ // Keep appUserAuthenticated in sync
105
+ useTask$(({ track }) => {
106
+ const u = track(() => appUser.value);
107
+ appUserAuthenticated.value = !!u;
108
+ });
58
109
 
59
110
  useContextProvider(DouveryAuthContext, {
60
111
  state,
61
112
  isInitialized,
62
113
  isLoading,
63
114
  error,
64
- client,
115
+ clientRef,
116
+ appUser,
117
+ appUserAuthenticated,
65
118
  });
66
119
 
67
- useVisibleTask$(() => {
68
- client
69
- .initialize()
70
- .then(() => {
71
- isInitialized.value = true;
72
- state.value = client.getState();
73
- })
74
- .catch((err) => {
75
- error.value = err instanceof Error ? err : new Error(String(err));
76
- });
120
+ // Client creation deferred to browser-only task.
121
+ // The QRL is invoked here, returning the full config (with customStorage).
122
+ // noSerialize() wraps the client so Qwik doesn't try to serialize it.
123
+ useVisibleTask$(async () => {
124
+ const config = await config$();
125
+ const client = createDouveryAuth(config);
126
+ clientRef.value = noSerialize(client);
127
+
128
+ try {
129
+ await client.initialize();
130
+ isInitialized.value = true;
131
+ state.value = client.getState();
132
+ } catch (err) {
133
+ error.value = err instanceof Error ? err : new Error(String(err));
134
+ }
77
135
 
78
136
  const unsubscribe = client.subscribe((event) => {
79
137
  state.value = client.getState();
@@ -93,10 +151,29 @@ export const DouveryAuthProvider = component$<DouveryAuthProviderProps>(
93
151
  },
94
152
  );
95
153
 
154
+ // ============================================================================
155
+ // Hooks
156
+ // ============================================================================
157
+
96
158
  export function useDouveryAuth() {
97
159
  return useContext(DouveryAuthContext);
98
160
  }
99
161
 
162
+ /**
163
+ * Internal helper: safely access the client from context.
164
+ * Throws if the client hasn't been initialized yet (before useVisibleTask$ runs).
165
+ */
166
+ function getClient(ctx: DouveryAuthContextValue): DouveryAuthClient {
167
+ const client = ctx.clientRef.value;
168
+ if (!client) {
169
+ throw new Error(
170
+ "DouveryAuth client not initialized. " +
171
+ "Ensure DouveryAuthProvider is mounted and the page has hydrated.",
172
+ );
173
+ }
174
+ return client;
175
+ }
176
+
100
177
  export function useUser(): Signal<User | null> {
101
178
  const { state } = useDouveryAuth();
102
179
  const user = useSignal<User | null>(state.value.user);
@@ -118,9 +195,11 @@ export function useIsAuthenticated(): Signal<boolean> {
118
195
  }
119
196
 
120
197
  export function useAuthActions() {
121
- const { client, isLoading, error } = useDouveryAuth();
198
+ const ctx = useDouveryAuth();
199
+ const { isLoading, error } = ctx;
122
200
 
123
- const login = async (options?: LoginOptions) => {
201
+ const login = $(async (options?: LoginOptions) => {
202
+ const client = getClient(ctx);
124
203
  isLoading.value = true;
125
204
  error.value = null;
126
205
  try {
@@ -131,9 +210,10 @@ export function useAuthActions() {
131
210
  } finally {
132
211
  isLoading.value = false;
133
212
  }
134
- };
213
+ });
135
214
 
136
- const logout = async (options?: LogoutOptions) => {
215
+ const logout = $(async (options?: LogoutOptions) => {
216
+ const client = getClient(ctx);
137
217
  isLoading.value = true;
138
218
  error.value = null;
139
219
  try {
@@ -144,41 +224,42 @@ export function useAuthActions() {
144
224
  } finally {
145
225
  isLoading.value = false;
146
226
  }
147
- };
227
+ });
148
228
 
149
- const selectAccount = (options?: SelectAccountOptions) => {
150
- client.selectAccount(options);
151
- };
229
+ const selectAccount = $((options?: SelectAccountOptions) => {
230
+ getClient(ctx).selectAccount(options);
231
+ });
152
232
 
153
- const addAccount = (options?: AddAccountOptions) => {
154
- client.addAccount(options);
155
- };
233
+ const addAccount = $((options?: AddAccountOptions) => {
234
+ getClient(ctx).addAccount(options);
235
+ });
156
236
 
157
- const register = (options?: RegisterOptions) => {
158
- client.register(options);
159
- };
237
+ const register = $((options?: RegisterOptions) => {
238
+ getClient(ctx).register(options);
239
+ });
160
240
 
161
- const recoverAccount = (options?: RecoverAccountOptions) => {
162
- client.recoverAccount(options);
163
- };
241
+ const recoverAccount = $((options?: RecoverAccountOptions) => {
242
+ getClient(ctx).recoverAccount(options);
243
+ });
164
244
 
165
- const verifyAccount = (options?: VerifyAccountOptions) => {
166
- client.verifyAccount(options);
167
- };
245
+ const verifyAccount = $((options?: VerifyAccountOptions) => {
246
+ getClient(ctx).verifyAccount(options);
247
+ });
168
248
 
169
- const upgradeAccount = (options?: UpgradeAccountOptions) => {
170
- client.upgradeAccount(options);
171
- };
249
+ const upgradeAccount = $((options?: UpgradeAccountOptions) => {
250
+ getClient(ctx).upgradeAccount(options);
251
+ });
172
252
 
173
- const setupPasskey = (options?: SetupPasskeyOptions) => {
174
- client.setupPasskey(options);
175
- };
253
+ const setupPasskey = $((options?: SetupPasskeyOptions) => {
254
+ getClient(ctx).setupPasskey(options);
255
+ });
176
256
 
177
- const setupAddress = (options?: SetupAddressOptions) => {
178
- client.setupAddress(options);
179
- };
257
+ const setupAddress = $((options?: SetupAddressOptions) => {
258
+ getClient(ctx).setupAddress(options);
259
+ });
180
260
 
181
- const revokeToken = async (options?: RevokeTokenOptions) => {
261
+ const revokeToken = $(async (options?: RevokeTokenOptions) => {
262
+ const client = getClient(ctx);
182
263
  isLoading.value = true;
183
264
  error.value = null;
184
265
  try {
@@ -189,7 +270,7 @@ export function useAuthActions() {
189
270
  } finally {
190
271
  isLoading.value = false;
191
272
  }
192
- };
273
+ });
193
274
 
194
275
  return {
195
276
  login,
@@ -209,48 +290,135 @@ export function useAuthActions() {
209
290
 
210
291
  /** Get URL builders for auth pages (non-redirecting, useful for <a> tags) */
211
292
  export function useAuthUrls() {
212
- const { client } = useDouveryAuth();
293
+ const ctx = useDouveryAuth();
213
294
  return {
214
- loginUrl: (options?: LoginOptions): AuthUrl =>
215
- client.buildLoginUrl(options),
216
- logoutUrl: (options?: LogoutOptions): AuthUrl =>
217
- client.buildLogoutUrl(options),
218
- selectAccountUrl: (options?: SelectAccountOptions): AuthUrl =>
219
- client.buildSelectAccountUrl(options),
220
- addAccountUrl: (options?: AddAccountOptions): AuthUrl =>
221
- client.buildAddAccountUrl(options),
222
- registerUrl: (options?: RegisterOptions): AuthUrl =>
223
- client.buildRegisterUrl(options),
224
- recoverAccountUrl: (options?: RecoverAccountOptions): AuthUrl =>
225
- client.buildRecoverAccountUrl(options),
226
- verifyAccountUrl: (options?: VerifyAccountOptions): AuthUrl =>
227
- client.buildVerifyAccountUrl(options),
228
- upgradeAccountUrl: (options?: UpgradeAccountOptions): AuthUrl =>
229
- client.buildUpgradeAccountUrl(options),
230
- setupPasskeyUrl: (options?: SetupPasskeyOptions): AuthUrl =>
231
- client.buildSetupPasskeyUrl(options),
232
- setupAddressUrl: (options?: SetupAddressOptions): AuthUrl =>
233
- client.buildSetupAddressUrl(options),
295
+ loginUrl: $(
296
+ (options?: LoginOptions): AuthUrl =>
297
+ getClient(ctx).buildLoginUrl(options),
298
+ ),
299
+ logoutUrl: $(
300
+ (options?: LogoutOptions): AuthUrl =>
301
+ getClient(ctx).buildLogoutUrl(options),
302
+ ),
303
+ selectAccountUrl: $(
304
+ (options?: SelectAccountOptions): AuthUrl =>
305
+ getClient(ctx).buildSelectAccountUrl(options),
306
+ ),
307
+ addAccountUrl: $(
308
+ (options?: AddAccountOptions): AuthUrl =>
309
+ getClient(ctx).buildAddAccountUrl(options),
310
+ ),
311
+ registerUrl: $(
312
+ (options?: RegisterOptions): AuthUrl =>
313
+ getClient(ctx).buildRegisterUrl(options),
314
+ ),
315
+ recoverAccountUrl: $(
316
+ (options?: RecoverAccountOptions): AuthUrl =>
317
+ getClient(ctx).buildRecoverAccountUrl(options),
318
+ ),
319
+ verifyAccountUrl: $(
320
+ (options?: VerifyAccountOptions): AuthUrl =>
321
+ getClient(ctx).buildVerifyAccountUrl(options),
322
+ ),
323
+ upgradeAccountUrl: $(
324
+ (options?: UpgradeAccountOptions): AuthUrl =>
325
+ getClient(ctx).buildUpgradeAccountUrl(options),
326
+ ),
327
+ setupPasskeyUrl: $(
328
+ (options?: SetupPasskeyOptions): AuthUrl =>
329
+ getClient(ctx).buildSetupPasskeyUrl(options),
330
+ ),
331
+ setupAddressUrl: $(
332
+ (options?: SetupAddressOptions): AuthUrl =>
333
+ getClient(ctx).buildSetupAddressUrl(options),
334
+ ),
234
335
  };
235
336
  }
236
337
 
237
338
  /** Get session status helpers */
238
339
  export function useSessionStatus() {
239
- const { client, state } = useDouveryAuth();
240
- const isExpired = useSignal(client.isSessionExpired());
241
- const needsVerification = useSignal(client.needsEmailVerification());
242
- const isGuest = useSignal(client.isGuestAccount());
340
+ const ctx = useDouveryAuth();
341
+ const { state } = ctx;
342
+ const c = ctx.clientRef.value;
343
+ const isExpired = useSignal(c ? c.isSessionExpired() : false);
344
+ const needsVerification = useSignal(c ? c.needsEmailVerification() : false);
345
+ const isGuest = useSignal(c ? c.isGuestAccount() : false);
243
346
 
244
347
  useTask$(({ track }) => {
245
348
  track(() => state.value);
246
- isExpired.value = client.isSessionExpired();
247
- needsVerification.value = client.needsEmailVerification();
248
- isGuest.value = client.isGuestAccount();
349
+ const client = ctx.clientRef.value;
350
+ if (client) {
351
+ isExpired.value = client.isSessionExpired();
352
+ needsVerification.value = client.needsEmailVerification();
353
+ isGuest.value = client.isGuestAccount();
354
+ }
249
355
  });
250
356
 
251
357
  return { isExpired, needsVerification, isGuest };
252
358
  }
253
359
 
360
+ // ============================================================================
361
+ // App User hooks
362
+ // ============================================================================
363
+
364
+ /**
365
+ * Returns the application-specific user data provided via `appUser` prop.
366
+ * Cast to your app's user type: `const user = useAppUser<UserACC>()`.
367
+ * Returns `{ user: Signal<T | null>, isAuthenticated: Signal<boolean> }`.
368
+ */
369
+ export function useAppUser<T = unknown>() {
370
+ const { appUser, appUserAuthenticated } = useDouveryAuth();
371
+ return {
372
+ user: appUser as Signal<T | null>,
373
+ isAuthenticated: appUserAuthenticated,
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Full app user context with refresh capabilities.
379
+ * Use when you need to re-fetch user data from the server.
380
+ */
381
+ export function useAppUserActions<T = unknown>() {
382
+ const { appUser, appUserAuthenticated } = useDouveryAuth();
383
+
384
+ const updateUser = $((userData: T | null) => {
385
+ (appUser as Signal<T | null>).value = userData;
386
+ appUserAuthenticated.value = !!userData;
387
+ });
388
+
389
+ const refreshUser = $(async () => {
390
+ try {
391
+ const response = await fetch("/api/auth/me", {
392
+ method: "GET",
393
+ credentials: "include",
394
+ headers: { "Cache-Control": "no-cache", Pragma: "no-cache" },
395
+ });
396
+
397
+ if (response.ok) {
398
+ const data = await response.json();
399
+ (appUser as Signal<T | null>).value = data.user;
400
+ appUserAuthenticated.value = true;
401
+ } else if (response.status === 401 || response.status === 403) {
402
+ (appUser as Signal<T | null>).value = null;
403
+ appUserAuthenticated.value = false;
404
+ }
405
+ } catch {
406
+ // Network error: keep current state
407
+ }
408
+ });
409
+
410
+ return {
411
+ user: appUser as Signal<T | null>,
412
+ isAuthenticated: appUserAuthenticated,
413
+ updateUser,
414
+ refreshUser,
415
+ };
416
+ }
417
+
418
+ // ============================================================================
419
+ // Re-exports
420
+ // ============================================================================
421
+
254
422
  export { DouveryAuthClient, createDouveryAuth } from "@douvery/auth";
255
423
  export type {
256
424
  DouveryAuthConfig,