@douvery/auth 0.3.2 → 0.4.0
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/dist/index.d.ts +176 -1
- package/dist/index.js +137 -2
- package/dist/index.js.map +1 -1
- package/dist/qwik/index.d.ts +100 -27
- package/dist/qwik/index.js +183 -59
- package/dist/qwik/index.js.map +1 -1
- package/dist/session/index.d.ts +205 -0
- package/dist/session/index.js +311 -0
- package/dist/session/index.js.map +1 -0
- package/package.json +17 -11
- package/src/qwik/index.tsx +263 -75
- package/src/qwik/session.ts +72 -0
package/src/qwik/index.tsx
CHANGED
|
@@ -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,111 @@ 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
const state = useSignal<AuthState>(
|
|
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
|
-
|
|
115
|
+
clientRef,
|
|
116
|
+
appUser,
|
|
117
|
+
appUserAuthenticated,
|
|
65
118
|
});
|
|
66
119
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
let config: DouveryAuthConfig | undefined;
|
|
125
|
+
try {
|
|
126
|
+
config = await config$();
|
|
127
|
+
} catch (err) {
|
|
128
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!config) {
|
|
133
|
+
error.value = new Error(
|
|
134
|
+
"[DouveryAuthProvider] config$() returned undefined. " +
|
|
135
|
+
"Check that the QRL correctly returns a DouveryAuthConfig object.",
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const client = createDouveryAuth(config);
|
|
141
|
+
clientRef.value = noSerialize(client);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await client.initialize();
|
|
145
|
+
isInitialized.value = true;
|
|
146
|
+
state.value = client.getState();
|
|
147
|
+
} catch (err) {
|
|
148
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
149
|
+
}
|
|
77
150
|
|
|
78
151
|
const unsubscribe = client.subscribe((event) => {
|
|
79
152
|
state.value = client.getState();
|
|
@@ -93,10 +166,29 @@ export const DouveryAuthProvider = component$<DouveryAuthProviderProps>(
|
|
|
93
166
|
},
|
|
94
167
|
);
|
|
95
168
|
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// Hooks
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
96
173
|
export function useDouveryAuth() {
|
|
97
174
|
return useContext(DouveryAuthContext);
|
|
98
175
|
}
|
|
99
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Internal helper: safely access the client from context.
|
|
179
|
+
* Throws if the client hasn't been initialized yet (before useVisibleTask$ runs).
|
|
180
|
+
*/
|
|
181
|
+
function getClient(ctx: DouveryAuthContextValue): DouveryAuthClient {
|
|
182
|
+
const client = ctx.clientRef.value;
|
|
183
|
+
if (!client) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
"DouveryAuth client not initialized. " +
|
|
186
|
+
"Ensure DouveryAuthProvider is mounted and the page has hydrated.",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return client;
|
|
190
|
+
}
|
|
191
|
+
|
|
100
192
|
export function useUser(): Signal<User | null> {
|
|
101
193
|
const { state } = useDouveryAuth();
|
|
102
194
|
const user = useSignal<User | null>(state.value.user);
|
|
@@ -118,9 +210,11 @@ export function useIsAuthenticated(): Signal<boolean> {
|
|
|
118
210
|
}
|
|
119
211
|
|
|
120
212
|
export function useAuthActions() {
|
|
121
|
-
const
|
|
213
|
+
const ctx = useDouveryAuth();
|
|
214
|
+
const { isLoading, error } = ctx;
|
|
122
215
|
|
|
123
|
-
const login = async (options?: LoginOptions) => {
|
|
216
|
+
const login = $(async (options?: LoginOptions) => {
|
|
217
|
+
const client = getClient(ctx);
|
|
124
218
|
isLoading.value = true;
|
|
125
219
|
error.value = null;
|
|
126
220
|
try {
|
|
@@ -131,9 +225,10 @@ export function useAuthActions() {
|
|
|
131
225
|
} finally {
|
|
132
226
|
isLoading.value = false;
|
|
133
227
|
}
|
|
134
|
-
};
|
|
228
|
+
});
|
|
135
229
|
|
|
136
|
-
const logout = async (options?: LogoutOptions) => {
|
|
230
|
+
const logout = $(async (options?: LogoutOptions) => {
|
|
231
|
+
const client = getClient(ctx);
|
|
137
232
|
isLoading.value = true;
|
|
138
233
|
error.value = null;
|
|
139
234
|
try {
|
|
@@ -144,41 +239,42 @@ export function useAuthActions() {
|
|
|
144
239
|
} finally {
|
|
145
240
|
isLoading.value = false;
|
|
146
241
|
}
|
|
147
|
-
};
|
|
242
|
+
});
|
|
148
243
|
|
|
149
|
-
const selectAccount = (options?: SelectAccountOptions) => {
|
|
150
|
-
|
|
151
|
-
};
|
|
244
|
+
const selectAccount = $((options?: SelectAccountOptions) => {
|
|
245
|
+
getClient(ctx).selectAccount(options);
|
|
246
|
+
});
|
|
152
247
|
|
|
153
|
-
const addAccount = (options?: AddAccountOptions) => {
|
|
154
|
-
|
|
155
|
-
};
|
|
248
|
+
const addAccount = $((options?: AddAccountOptions) => {
|
|
249
|
+
getClient(ctx).addAccount(options);
|
|
250
|
+
});
|
|
156
251
|
|
|
157
|
-
const register = (options?: RegisterOptions) => {
|
|
158
|
-
|
|
159
|
-
};
|
|
252
|
+
const register = $((options?: RegisterOptions) => {
|
|
253
|
+
getClient(ctx).register(options);
|
|
254
|
+
});
|
|
160
255
|
|
|
161
|
-
const recoverAccount = (options?: RecoverAccountOptions) => {
|
|
162
|
-
|
|
163
|
-
};
|
|
256
|
+
const recoverAccount = $((options?: RecoverAccountOptions) => {
|
|
257
|
+
getClient(ctx).recoverAccount(options);
|
|
258
|
+
});
|
|
164
259
|
|
|
165
|
-
const verifyAccount = (options?: VerifyAccountOptions) => {
|
|
166
|
-
|
|
167
|
-
};
|
|
260
|
+
const verifyAccount = $((options?: VerifyAccountOptions) => {
|
|
261
|
+
getClient(ctx).verifyAccount(options);
|
|
262
|
+
});
|
|
168
263
|
|
|
169
|
-
const upgradeAccount = (options?: UpgradeAccountOptions) => {
|
|
170
|
-
|
|
171
|
-
};
|
|
264
|
+
const upgradeAccount = $((options?: UpgradeAccountOptions) => {
|
|
265
|
+
getClient(ctx).upgradeAccount(options);
|
|
266
|
+
});
|
|
172
267
|
|
|
173
|
-
const setupPasskey = (options?: SetupPasskeyOptions) => {
|
|
174
|
-
|
|
175
|
-
};
|
|
268
|
+
const setupPasskey = $((options?: SetupPasskeyOptions) => {
|
|
269
|
+
getClient(ctx).setupPasskey(options);
|
|
270
|
+
});
|
|
176
271
|
|
|
177
|
-
const setupAddress = (options?: SetupAddressOptions) => {
|
|
178
|
-
|
|
179
|
-
};
|
|
272
|
+
const setupAddress = $((options?: SetupAddressOptions) => {
|
|
273
|
+
getClient(ctx).setupAddress(options);
|
|
274
|
+
});
|
|
180
275
|
|
|
181
|
-
const revokeToken = async (options?: RevokeTokenOptions) => {
|
|
276
|
+
const revokeToken = $(async (options?: RevokeTokenOptions) => {
|
|
277
|
+
const client = getClient(ctx);
|
|
182
278
|
isLoading.value = true;
|
|
183
279
|
error.value = null;
|
|
184
280
|
try {
|
|
@@ -189,7 +285,7 @@ export function useAuthActions() {
|
|
|
189
285
|
} finally {
|
|
190
286
|
isLoading.value = false;
|
|
191
287
|
}
|
|
192
|
-
};
|
|
288
|
+
});
|
|
193
289
|
|
|
194
290
|
return {
|
|
195
291
|
login,
|
|
@@ -209,48 +305,135 @@ export function useAuthActions() {
|
|
|
209
305
|
|
|
210
306
|
/** Get URL builders for auth pages (non-redirecting, useful for <a> tags) */
|
|
211
307
|
export function useAuthUrls() {
|
|
212
|
-
const
|
|
308
|
+
const ctx = useDouveryAuth();
|
|
213
309
|
return {
|
|
214
|
-
loginUrl: (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
310
|
+
loginUrl: $(
|
|
311
|
+
(options?: LoginOptions): AuthUrl =>
|
|
312
|
+
getClient(ctx).buildLoginUrl(options),
|
|
313
|
+
),
|
|
314
|
+
logoutUrl: $(
|
|
315
|
+
(options?: LogoutOptions): AuthUrl =>
|
|
316
|
+
getClient(ctx).buildLogoutUrl(options),
|
|
317
|
+
),
|
|
318
|
+
selectAccountUrl: $(
|
|
319
|
+
(options?: SelectAccountOptions): AuthUrl =>
|
|
320
|
+
getClient(ctx).buildSelectAccountUrl(options),
|
|
321
|
+
),
|
|
322
|
+
addAccountUrl: $(
|
|
323
|
+
(options?: AddAccountOptions): AuthUrl =>
|
|
324
|
+
getClient(ctx).buildAddAccountUrl(options),
|
|
325
|
+
),
|
|
326
|
+
registerUrl: $(
|
|
327
|
+
(options?: RegisterOptions): AuthUrl =>
|
|
328
|
+
getClient(ctx).buildRegisterUrl(options),
|
|
329
|
+
),
|
|
330
|
+
recoverAccountUrl: $(
|
|
331
|
+
(options?: RecoverAccountOptions): AuthUrl =>
|
|
332
|
+
getClient(ctx).buildRecoverAccountUrl(options),
|
|
333
|
+
),
|
|
334
|
+
verifyAccountUrl: $(
|
|
335
|
+
(options?: VerifyAccountOptions): AuthUrl =>
|
|
336
|
+
getClient(ctx).buildVerifyAccountUrl(options),
|
|
337
|
+
),
|
|
338
|
+
upgradeAccountUrl: $(
|
|
339
|
+
(options?: UpgradeAccountOptions): AuthUrl =>
|
|
340
|
+
getClient(ctx).buildUpgradeAccountUrl(options),
|
|
341
|
+
),
|
|
342
|
+
setupPasskeyUrl: $(
|
|
343
|
+
(options?: SetupPasskeyOptions): AuthUrl =>
|
|
344
|
+
getClient(ctx).buildSetupPasskeyUrl(options),
|
|
345
|
+
),
|
|
346
|
+
setupAddressUrl: $(
|
|
347
|
+
(options?: SetupAddressOptions): AuthUrl =>
|
|
348
|
+
getClient(ctx).buildSetupAddressUrl(options),
|
|
349
|
+
),
|
|
234
350
|
};
|
|
235
351
|
}
|
|
236
352
|
|
|
237
353
|
/** Get session status helpers */
|
|
238
354
|
export function useSessionStatus() {
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
const
|
|
355
|
+
const ctx = useDouveryAuth();
|
|
356
|
+
const { state } = ctx;
|
|
357
|
+
const c = ctx.clientRef.value;
|
|
358
|
+
const isExpired = useSignal(c ? c.isSessionExpired() : false);
|
|
359
|
+
const needsVerification = useSignal(c ? c.needsEmailVerification() : false);
|
|
360
|
+
const isGuest = useSignal(c ? c.isGuestAccount() : false);
|
|
243
361
|
|
|
244
362
|
useTask$(({ track }) => {
|
|
245
363
|
track(() => state.value);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
364
|
+
const client = ctx.clientRef.value;
|
|
365
|
+
if (client) {
|
|
366
|
+
isExpired.value = client.isSessionExpired();
|
|
367
|
+
needsVerification.value = client.needsEmailVerification();
|
|
368
|
+
isGuest.value = client.isGuestAccount();
|
|
369
|
+
}
|
|
249
370
|
});
|
|
250
371
|
|
|
251
372
|
return { isExpired, needsVerification, isGuest };
|
|
252
373
|
}
|
|
253
374
|
|
|
375
|
+
// ============================================================================
|
|
376
|
+
// App User hooks
|
|
377
|
+
// ============================================================================
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Returns the application-specific user data provided via `appUser` prop.
|
|
381
|
+
* Cast to your app's user type: `const user = useAppUser<UserACC>()`.
|
|
382
|
+
* Returns `{ user: Signal<T | null>, isAuthenticated: Signal<boolean> }`.
|
|
383
|
+
*/
|
|
384
|
+
export function useAppUser<T = unknown>() {
|
|
385
|
+
const { appUser, appUserAuthenticated } = useDouveryAuth();
|
|
386
|
+
return {
|
|
387
|
+
user: appUser as Signal<T | null>,
|
|
388
|
+
isAuthenticated: appUserAuthenticated,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Full app user context with refresh capabilities.
|
|
394
|
+
* Use when you need to re-fetch user data from the server.
|
|
395
|
+
*/
|
|
396
|
+
export function useAppUserActions<T = unknown>() {
|
|
397
|
+
const { appUser, appUserAuthenticated } = useDouveryAuth();
|
|
398
|
+
|
|
399
|
+
const updateUser = $((userData: T | null) => {
|
|
400
|
+
(appUser as Signal<T | null>).value = userData;
|
|
401
|
+
appUserAuthenticated.value = !!userData;
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const refreshUser = $(async () => {
|
|
405
|
+
try {
|
|
406
|
+
const response = await fetch("/api/auth/me", {
|
|
407
|
+
method: "GET",
|
|
408
|
+
credentials: "include",
|
|
409
|
+
headers: { "Cache-Control": "no-cache", Pragma: "no-cache" },
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
if (response.ok) {
|
|
413
|
+
const data = await response.json();
|
|
414
|
+
(appUser as Signal<T | null>).value = data.user;
|
|
415
|
+
appUserAuthenticated.value = true;
|
|
416
|
+
} else if (response.status === 401 || response.status === 403) {
|
|
417
|
+
(appUser as Signal<T | null>).value = null;
|
|
418
|
+
appUserAuthenticated.value = false;
|
|
419
|
+
}
|
|
420
|
+
} catch {
|
|
421
|
+
// Network error: keep current state
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
user: appUser as Signal<T | null>,
|
|
427
|
+
isAuthenticated: appUserAuthenticated,
|
|
428
|
+
updateUser,
|
|
429
|
+
refreshUser,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ============================================================================
|
|
434
|
+
// Re-exports
|
|
435
|
+
// ============================================================================
|
|
436
|
+
|
|
254
437
|
export { DouveryAuthClient, createDouveryAuth } from "@douvery/auth";
|
|
255
438
|
export type {
|
|
256
439
|
DouveryAuthConfig,
|
|
@@ -268,4 +451,9 @@ export type {
|
|
|
268
451
|
AddAccountOptions,
|
|
269
452
|
RevokeTokenOptions,
|
|
270
453
|
AuthUrl,
|
|
454
|
+
CookieAdapter,
|
|
455
|
+
CookieSetOptions,
|
|
271
456
|
} from "@douvery/auth";
|
|
457
|
+
|
|
458
|
+
// Session adapter for Qwik City
|
|
459
|
+
export { createQwikSessionAdapter } from "./session";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @douvery/auth/qwik - Session Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapts Qwik City's Cookie interface to the generic CookieAdapter
|
|
5
|
+
* used by createSessionResolver().
|
|
6
|
+
*
|
|
7
|
+
* Memoized: returns the same adapter instance for the same Cookie object,
|
|
8
|
+
* ensuring the resolver's per-request WeakMap cache works correctly when
|
|
9
|
+
* multiple routeLoaders call getAccessToken() in the same SSR request.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CookieAdapter, CookieSetOptions } from "@douvery/auth";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Qwik City Cookie-like interface.
|
|
16
|
+
* Duck-typed to avoid hard dependency on @builder.io/qwik-city.
|
|
17
|
+
*/
|
|
18
|
+
interface QwikCookieLike {
|
|
19
|
+
get(name: string): { value: string } | null;
|
|
20
|
+
set(
|
|
21
|
+
name: string,
|
|
22
|
+
value: string | number | Record<string, unknown>,
|
|
23
|
+
options?: Record<string, unknown>,
|
|
24
|
+
): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Adapter cache ensures the SAME CookieAdapter instance is returned
|
|
29
|
+
* for the same Qwik Cookie object. This is critical because:
|
|
30
|
+
*
|
|
31
|
+
* 1. The resolver uses WeakMap<CookieAdapter> for per-request caching
|
|
32
|
+
* 2. Multiple routeLoaders in the same SSR request share the same Cookie
|
|
33
|
+
* 3. Each routeLoader calls createQwikSessionAdapter(cookie)
|
|
34
|
+
* 4. Without memoization, each call would create a different object
|
|
35
|
+
* → WeakMap would fail to deduplicate → duplicate network calls
|
|
36
|
+
*/
|
|
37
|
+
const adapterCache = new WeakMap<object, CookieAdapter>();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a CookieAdapter from a Qwik City Cookie object.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* import { createQwikSessionAdapter } from '@douvery/auth/qwik';
|
|
45
|
+
* import { createSessionResolver } from '@douvery/auth/session';
|
|
46
|
+
*
|
|
47
|
+
* const resolver = createSessionResolver({ ... });
|
|
48
|
+
*
|
|
49
|
+
* export const useMyLoader = routeLoader$(async ({ cookie }) => {
|
|
50
|
+
* const adapter = createQwikSessionAdapter(cookie);
|
|
51
|
+
* const token = await resolver.getAccessToken(adapter);
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function createQwikSessionAdapter(
|
|
56
|
+
cookie: QwikCookieLike,
|
|
57
|
+
): CookieAdapter {
|
|
58
|
+
let adapter = adapterCache.get(cookie);
|
|
59
|
+
if (adapter) return adapter;
|
|
60
|
+
|
|
61
|
+
adapter = {
|
|
62
|
+
get(name: string): string | undefined {
|
|
63
|
+
return cookie.get(name)?.value ?? undefined;
|
|
64
|
+
},
|
|
65
|
+
set(name: string, value: string, options: CookieSetOptions): void {
|
|
66
|
+
cookie.set(name, value, options as Record<string, unknown>);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
adapterCache.set(cookie, adapter);
|
|
71
|
+
return adapter;
|
|
72
|
+
}
|