@douvery/auth 0.2.0 → 0.3.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/README.md CHANGED
@@ -1,88 +1,117 @@
1
1
  # @douvery/auth
2
2
 
3
- OAuth 2.0/OIDC client library for Douvery authentication.
3
+ <p align="center">
4
+ <img src="https://img.shields.io/npm/v/@douvery/auth?style=flat-square&color=blue" alt="npm version" />
5
+ <img src="https://img.shields.io/npm/dm/@douvery/auth?style=flat-square&color=green" alt="downloads" />
6
+ <img src="https://img.shields.io/npm/l/@douvery/auth?style=flat-square" alt="license" />
7
+ <img src="https://img.shields.io/badge/TypeScript-5.0+-blue?style=flat-square&logo=typescript" alt="typescript" />
8
+ </p>
9
+
10
+ <p align="center">
11
+ <strong>🔐 OAuth 2.0/OIDC client library for Douvery authentication</strong>
12
+ </p>
13
+
14
+ <p align="center">
15
+ Secure, type-safe authentication with PKCE support for React, Qwik, and vanilla TypeScript.
16
+ </p>
17
+
18
+ ---
19
+
20
+ ## âœĻ Features
21
+
22
+ - 🔒 **PKCE Support** - Secure authorization code flow with Proof Key for Code Exchange
23
+ - 🔄 **Auto Token Refresh** - Automatic token refresh before expiry
24
+ - ðŸ’ū **Multiple Storage Options** - localStorage, sessionStorage, memory, or cookies
25
+ - ðŸ“Ķ **Tree Shakeable** - Import only what you need
26
+ - ðŸŽŊ **TypeScript First** - Full TypeScript support with comprehensive types
27
+ - ⚛ïļ **React Adapter** - Provider and hooks for React 18+
28
+ - ⚡ **Qwik Adapter** - Signal-based reactivity for Qwik
29
+ - ðŸ“Ą **Event System** - Subscribe to auth events (login, logout, token refresh)
30
+ - 🌐 **SSR Compatible** - Works with server-side rendering
31
+ - ðŸŠķ **Lightweight** - ~23KB core, framework adapters add minimal overhead
32
+ - 🧭 **Auth Navigation** - Built-in helpers for all auth flows (register, recover, verify, switch account, etc.)
33
+ - 🔗 **URL Builders** - Generate auth URLs for `<a>` tags without programmatic redirect
34
+ - 🔑 **Token Revocation** - Revoke access/refresh tokens via RFC 7009
35
+
36
+ ---
37
+
38
+ ## ðŸ“Ķ Installation
4
39
 
5
- ## Packages
40
+ ```bash
41
+ # npm
42
+ npm install @douvery/auth
6
43
 
7
- | Package | Description |
8
- |---------|-------------|
9
- | [@douvery/auth](./packages/core) | Core client library (vanilla TypeScript) |
10
- | [@douvery/auth-react](./packages/react) | React hooks and components |
11
- | [@douvery/auth-qwik](./packages/qwik) | Qwik hooks and components |
44
+ # pnpm
45
+ pnpm add @douvery/auth
12
46
 
13
- ## Installation
47
+ # bun
48
+ bun add @douvery/auth
14
49
 
15
- ### React
16
-
17
- ```bash
18
- npm install @douvery/auth-react
19
- # or
20
- pnpm add @douvery/auth-react
50
+ # yarn
51
+ yarn add @douvery/auth
21
52
  ```
22
53
 
23
- ### Qwik
54
+ ### Peer Dependencies (Optional)
24
55
 
25
- ```bash
26
- npm install @douvery/auth-qwik
27
- # or
28
- pnpm add @douvery/auth-qwik
29
- ```
30
-
31
- ### Vanilla TypeScript
56
+ - **React**: `react >= 18.0.0` (only if using `@douvery/auth/react`)
57
+ - **Qwik**: `@builder.io/qwik >= 1.0.0` (only if using `@douvery/auth/qwik`)
32
58
 
33
- ```bash
34
- npm install @douvery/auth
35
- # or
36
- pnpm add @douvery/auth
37
- ```
59
+ ---
38
60
 
39
- ## Quick Start
61
+ ## 🚀 Quick Start
40
62
 
41
63
  ### React
42
64
 
43
65
  ```tsx
44
- import { DouveryAuthProvider, useDouveryAuth } from '@douvery/auth-react';
66
+ import { DouveryAuthProvider, useDouveryAuth, useUser } from '@douvery/auth/react';
45
67
 
46
- // Wrap your app with the provider
68
+ // 1. Wrap your app with the provider
47
69
  function App() {
48
70
  return (
49
71
  <DouveryAuthProvider
50
72
  config={{
51
73
  clientId: 'your-client-id',
52
- redirectUri: 'http://localhost:3000/callback',
74
+ redirectUri: window.location.origin + '/callback',
53
75
  issuer: 'https://auth.douvery.com',
54
76
  scopes: ['openid', 'profile', 'email'],
55
77
  }}
78
+ onAuthenticated={(user) => console.log('Logged in:', user)}
79
+ onLogout={() => console.log('Logged out')}
80
+ onError={(error) => console.error('Auth error:', error)}
56
81
  >
57
82
  <YourApp />
58
83
  </DouveryAuthProvider>
59
84
  );
60
85
  }
61
86
 
62
- // Use the hook in your components
87
+ // 2. Use hooks in your components
63
88
  function LoginButton() {
64
- const { login, logout, isAuthenticated, user } = useDouveryAuth();
89
+ const { login, logout, isAuthenticated, isLoading } = useDouveryAuth();
90
+ const user = useUser();
91
+
92
+ if (isLoading) return <span>Loading...</span>;
65
93
 
66
94
  if (isAuthenticated) {
67
95
  return (
68
96
  <div>
97
+ <img src={user?.picture} alt={user?.name} />
69
98
  <p>Welcome, {user?.name}!</p>
70
99
  <button onClick={() => logout()}>Logout</button>
71
100
  </div>
72
101
  );
73
102
  }
74
103
 
75
- return <button onClick={() => login()}>Login</button>;
104
+ return <button onClick={() => login()}>Login with Douvery</button>;
76
105
  }
77
106
  ```
78
107
 
79
108
  ### Qwik
80
109
 
81
110
  ```tsx
82
- import { DouveryAuthProvider, useDouveryAuth } from '@douvery/auth-qwik';
83
- import { component$ } from '@builder.io/qwik';
111
+ import { DouveryAuthProvider, useDouveryAuth, useUser, useAuthActions } from '@douvery/auth/qwik';
112
+ import { component$, Slot } from '@builder.io/qwik';
84
113
 
85
- // Wrap your app with the provider
114
+ // 1. Wrap your app with the provider
86
115
  export default component$(() => {
87
116
  return (
88
117
  <DouveryAuthProvider
@@ -98,134 +127,653 @@ export default component$(() => {
98
127
  );
99
128
  });
100
129
 
101
- // Use the hook in your components
130
+ // 2. Use hooks in your components (signal-based)
102
131
  export const LoginButton = component$(() => {
103
- const { login, logout, isAuthenticated, user } = useDouveryAuth();
132
+ const user = useUser();
133
+ const { login, logout, isLoading } = useAuthActions();
104
134
 
105
135
  return (
106
136
  <>
107
- {isAuthenticated.value ? (
137
+ {user.value ? (
108
138
  <div>
109
- <p>Welcome, {user.value?.name}!</p>
139
+ <img src={user.value.picture} alt={user.value.name} />
140
+ <p>Welcome, {user.value.name}!</p>
110
141
  <button onClick$={() => logout()}>Logout</button>
111
142
  </div>
112
143
  ) : (
113
- <button onClick$={() => login()}>Login</button>
144
+ <button onClick$={() => login()} disabled={isLoading.value}>
145
+ {isLoading.value ? 'Loading...' : 'Login with Douvery'}
146
+ </button>
114
147
  )}
115
148
  </>
116
149
  );
117
150
  });
118
151
  ```
119
152
 
120
- ### Vanilla TypeScript
153
+ ### Vanilla TypeScript / JavaScript
121
154
 
122
155
  ```typescript
123
156
  import { createDouveryAuth } from '@douvery/auth';
124
157
 
158
+ // 1. Create the auth client
125
159
  const auth = createDouveryAuth({
126
160
  clientId: 'your-client-id',
127
- redirectUri: 'http://localhost:3000/callback',
161
+ redirectUri: window.location.origin + '/callback',
128
162
  issuer: 'https://auth.douvery.com',
129
163
  scopes: ['openid', 'profile', 'email'],
130
164
  });
131
165
 
132
- // Initialize (checks for existing session or handles callback)
166
+ // 2. Subscribe to auth events
167
+ auth.subscribe((event) => {
168
+ switch (event.type) {
169
+ case 'LOGIN_SUCCESS':
170
+ console.log('Logged in:', event.user);
171
+ break;
172
+ case 'LOGOUT_SUCCESS':
173
+ console.log('Logged out');
174
+ break;
175
+ case 'TOKEN_REFRESHED':
176
+ console.log('Token refreshed');
177
+ break;
178
+ case 'SESSION_EXPIRED':
179
+ console.log('Session expired');
180
+ break;
181
+ }
182
+ });
183
+
184
+ // 3. Initialize (handles callback if present, restores session)
133
185
  await auth.initialize();
134
186
 
135
- // Login
136
- await auth.login();
187
+ // 4. Check authentication state
188
+ const state = auth.getState();
189
+ console.log('Status:', state.status); // 'loading' | 'authenticated' | 'unauthenticated'
190
+ console.log('User:', state.user);
137
191
 
138
- // After callback, get user
139
- const user = auth.getUser();
140
- console.log(user);
192
+ // 5. Login (redirects to auth server)
193
+ await auth.login({ returnTo: '/dashboard' });
141
194
 
142
- // Get access token (auto-refreshes if needed)
195
+ // 6. Get access token (auto-refreshes if needed)
143
196
  const token = await auth.getAccessToken();
197
+ fetch('/api/protected', {
198
+ headers: { Authorization: `Bearer ${token}` }
199
+ });
144
200
 
145
- // Logout
201
+ // 7. Logout
146
202
  await auth.logout();
203
+
204
+ // 8. Navigation helpers — redirect to any auth flow
205
+ auth.selectAccount({ returnTo: '/dashboard' });
206
+ auth.addAccount({ loginHint: 'other@email.com' });
207
+ auth.register({ email: 'new@email.com' });
208
+ auth.recoverAccount({ email: 'user@email.com' });
209
+ auth.verifyAccount();
210
+ auth.upgradeAccount();
211
+ auth.setupPasskey();
212
+ auth.setupAddress();
213
+
214
+ // 9. URL builders (don't redirect, useful for <a> tags)
215
+ const url = auth.buildSelectAccountUrl({ returnTo: '/home' });
216
+ console.log(url.url); // "https://auth.douvery.com/select-account?continue=/home"
217
+ url.redirect(); // Navigate to the URL
218
+ url.open(); // Open in new tab
219
+
220
+ // 10. Revoke a token
221
+ await auth.revokeToken();
222
+ await auth.revokeToken({ tokenTypeHint: 'refresh_token' });
223
+
224
+ // 11. Session status helpers
225
+ auth.isSessionExpired(); // true/false
226
+ auth.needsEmailVerification(); // true/false
227
+ auth.isGuestAccount(); // true/false
147
228
  ```
148
229
 
149
- ## Configuration Options
230
+ ---
231
+
232
+ ## 📖 API Reference
233
+
234
+ ### Configuration
150
235
 
151
236
  ```typescript
152
237
  interface DouveryAuthConfig {
153
- /** OAuth Client ID */
238
+ /** OAuth Client ID (required) */
154
239
  clientId: string;
155
240
 
156
- /** Authorization server base URL (default: "https://auth.douvery.com") */
157
- issuer?: string;
158
-
159
- /** Redirect URI after authentication */
241
+ /** Redirect URI after authentication (required) */
160
242
  redirectUri: string;
161
243
 
244
+ /** Authorization server base URL */
245
+ issuer?: string; // default: "https://auth.douvery.com"
246
+
162
247
  /** Post-logout redirect URI */
163
248
  postLogoutRedirectUri?: string;
164
249
 
165
- /** OAuth scopes to request (default: ["openid", "profile", "email"]) */
166
- scopes?: string[];
250
+ /** OAuth scopes to request */
251
+ scopes?: string[]; // default: ["openid", "profile", "email"]
252
+
253
+ /** Token storage strategy */
254
+ storage?: "localStorage" | "sessionStorage" | "memory" | "cookie"; // default: "localStorage"
167
255
 
168
- /** Token storage strategy (default: "localStorage") */
169
- storage?: "localStorage" | "sessionStorage" | "memory" | "cookie";
256
+ /** Custom storage implementation */
257
+ customStorage?: TokenStorage;
170
258
 
171
- /** Auto-refresh tokens before expiry (default: true) */
172
- autoRefresh?: boolean;
259
+ /** Auto-refresh tokens before expiry */
260
+ autoRefresh?: boolean; // default: true
173
261
 
174
- /** Seconds before expiry to trigger refresh (default: 60) */
175
- refreshThreshold?: number;
262
+ /** Seconds before expiry to trigger refresh */
263
+ refreshThreshold?: number; // default: 60
176
264
 
177
- /** Enable debug logging (default: false) */
178
- debug?: boolean;
265
+ /** Enable debug logging */
266
+ debug?: boolean; // default: false
179
267
  }
180
268
  ```
181
269
 
182
- ## Login Options
270
+ ### Login Options
183
271
 
184
272
  ```typescript
185
273
  await auth.login({
186
274
  // URL to return to after login
187
275
  returnTo: '/dashboard',
188
276
 
189
- // Force re-authentication
190
- prompt: 'login',
277
+ // Force re-authentication or consent
278
+ prompt: 'login' | 'consent' | 'select_account' | 'none',
191
279
 
192
- // Pre-fill email
280
+ // Pre-fill email/username
193
281
  loginHint: 'user@example.com',
194
282
 
195
- // UI locale
283
+ // UI locale preference
196
284
  uiLocales: 'es',
285
+
286
+ // Maximum authentication age in seconds
287
+ maxAge: 3600,
288
+
289
+ // Additional authorization parameters
290
+ authorizationParams: {
291
+ audience: 'https://api.example.com',
292
+ },
293
+ });
294
+ ```
295
+
296
+ ### Logout Options
297
+
298
+ ```typescript
299
+ await auth.logout({
300
+ // URL to return to after logout
301
+ returnTo: 'https://example.com',
302
+
303
+ // End session at IdP (federated logout)
304
+ federated: true, // default: true
305
+
306
+ // Only clear local session, don't redirect
307
+ localOnly: false, // default: false
308
+ });
309
+ ```
310
+
311
+ ### Navigation Options
312
+
313
+ All navigation methods accept a common base of options:
314
+
315
+ ```typescript
316
+ interface AuthNavigationOptions {
317
+ returnTo?: string; // URL to return to after the action
318
+ clientId?: string; // OAuth client_id for branded experiences
319
+ openInNewTab?: boolean; // Open in new tab instead of redirect
320
+ }
321
+ ```
322
+
323
+ ```typescript
324
+ // Select / switch account
325
+ auth.selectAccount({
326
+ returnTo: '/dashboard',
327
+ loginHint: 'user@email.com', // Pre-select a specific account
328
+ });
329
+
330
+ // Add another account (multi-session)
331
+ auth.addAccount({
332
+ loginHint: 'another@email.com',
333
+ });
334
+
335
+ // Register a new account
336
+ auth.register({
337
+ email: 'new@email.com',
338
+ firstName: 'John',
339
+ lastName: 'Doe',
340
+ uiLocales: 'es',
341
+ });
342
+
343
+ // Recover account (forgot password)
344
+ auth.recoverAccount({
345
+ email: 'user@email.com',
346
+ });
347
+
348
+ // Verify account (email verification)
349
+ auth.verifyAccount({
350
+ email: 'user@email.com',
351
+ });
352
+
353
+ // Upgrade guest account to full account
354
+ auth.upgradeAccount({
355
+ scopes: ['profile', 'email'],
356
+ });
357
+
358
+ // Setup passkey
359
+ auth.setupPasskey({ returnTo: '/settings' });
360
+
361
+ // Setup address
362
+ auth.setupAddress({ returnTo: '/checkout' });
363
+ ```
364
+
365
+ ### URL Builders
366
+
367
+ Every navigation method has a corresponding `build*Url()` method that returns an `AuthUrl` object without redirecting. Useful for `<a>` tags or custom navigation logic:
368
+
369
+ ```typescript
370
+ const url = auth.buildRegisterUrl({ email: 'hint@test.com' });
371
+
372
+ url.url; // "https://auth.douvery.com/register?email=hint%40test.com"
373
+ url.redirect(); // window.location.href = url
374
+ url.open(); // window.open(url, '_blank')
375
+ ```
376
+
377
+ | Builder | Path |
378
+ |---|---|
379
+ | `buildLoginUrl()` | `/login` |
380
+ | `buildLogoutUrl()` | `/logout` |
381
+ | `buildSelectAccountUrl()` | `/select-account` |
382
+ | `buildAddAccountUrl()` | `/login?add_account=true` |
383
+ | `buildRegisterUrl()` | `/register` |
384
+ | `buildRecoverAccountUrl()` | `/recover-account` |
385
+ | `buildVerifyAccountUrl()` | `/verify-account` |
386
+ | `buildUpgradeAccountUrl()` | `/upgrade-account` |
387
+ | `buildSetupPasskeyUrl()` | `/setup-passkey` |
388
+ | `buildSetupAddressUrl()` | `/setup-address` |
389
+
390
+ ### Token Revocation
391
+
392
+ ```typescript
393
+ // Revoke current access token
394
+ await auth.revokeToken();
395
+
396
+ // Revoke a specific token
397
+ await auth.revokeToken({
398
+ token: 'specific-token-value',
399
+ tokenTypeHint: 'refresh_token',
197
400
  });
198
401
  ```
199
402
 
200
- ## Features
403
+ ### Session Status Helpers
404
+
405
+ ```typescript
406
+ auth.isSessionExpired(); // true if access token is expired
407
+ auth.needsEmailVerification(); // true if email is not verified
408
+ auth.isGuestAccount(); // true if account type is guest
409
+ ```
410
+
411
+ ### Auth State
412
+
413
+ ```typescript
414
+ interface AuthState {
415
+ status: 'loading' | 'authenticated' | 'unauthenticated';
416
+ user: User | null;
417
+ tokens: TokenInfo | null;
418
+ error: AuthError | null;
419
+ }
420
+
421
+ interface User {
422
+ id: string;
423
+ email?: string;
424
+ emailVerified?: boolean;
425
+ name?: string;
426
+ firstName?: string;
427
+ lastName?: string;
428
+ picture?: string;
429
+ phoneNumber?: string;
430
+ locale?: string;
431
+ [key: string]: unknown;
432
+ }
433
+ ```
434
+
435
+ ### Auth Events
436
+
437
+ ```typescript
438
+ type AuthEvent =
439
+ | { type: 'INITIALIZED' }
440
+ | { type: 'LOGIN_STARTED' }
441
+ | { type: 'LOGIN_SUCCESS'; user: User; tokens: TokenInfo }
442
+ | { type: 'LOGIN_ERROR'; error: AuthError }
443
+ | { type: 'LOGOUT_STARTED' }
444
+ | { type: 'LOGOUT_SUCCESS' }
445
+ | { type: 'LOGOUT_ERROR'; error: AuthError }
446
+ | { type: 'TOKEN_REFRESHED'; tokens: TokenInfo }
447
+ | { type: 'TOKEN_REFRESH_ERROR'; error: AuthError }
448
+ | { type: 'SESSION_EXPIRED' };
449
+ ```
450
+
451
+ ---
452
+
453
+ ## 🊝 React Hooks
454
+
455
+ | Hook | Description |
456
+ |------|-------------|
457
+ | `useDouveryAuth()` | Full context with state, actions, and client |
458
+ | `useUser()` | Current user or null |
459
+ | `useIsAuthenticated()` | Boolean authentication status |
460
+ | `useAccessToken()` | `{ accessToken, getAccessToken }` |
461
+ | `useAuthActions()` | All auth actions (see below) |
462
+ | `useAuthUrls()` | URL builders for all auth pages |
463
+ | `useSessionStatus()` | `{ isExpired, needsVerification, isGuest }` |
464
+
465
+ #### `useAuthActions()` — Full list
466
+
467
+ ```tsx
468
+ const {
469
+ login, // (options?: LoginOptions) => Promise<void>
470
+ logout, // (options?: LogoutOptions) => Promise<void>
471
+ selectAccount, // (options?: SelectAccountOptions) => void
472
+ addAccount, // (options?: AddAccountOptions) => void
473
+ register, // (options?: RegisterOptions) => void
474
+ recoverAccount, // (options?: RecoverAccountOptions) => void
475
+ verifyAccount, // (options?: VerifyAccountOptions) => void
476
+ upgradeAccount, // (options?: UpgradeAccountOptions) => void
477
+ setupPasskey, // (options?: SetupPasskeyOptions) => void
478
+ setupAddress, // (options?: SetupAddressOptions) => void
479
+ revokeToken, // (options?: RevokeTokenOptions) => Promise<void>
480
+ isLoading, // boolean
481
+ } = useAuthActions();
482
+ ```
483
+
484
+ #### `useAuthUrls()` — URL builders for links
201
485
 
202
- - ✅ **PKCE Support** - Secure authorization code flow with PKCE
203
- - ✅ **Auto Token Refresh** - Automatic token refresh before expiry
204
- - ✅ **Multiple Storage Options** - localStorage, sessionStorage, memory, or cookies
205
- - ✅ **TypeScript First** - Full TypeScript support with comprehensive types
206
- - ✅ **Framework Adapters** - React and Qwik adapters with hooks
207
- - ✅ **Event System** - Subscribe to auth events (login, logout, token refresh, etc.)
208
- - ✅ **SSR Compatible** - Works with server-side rendering
486
+ ```tsx
487
+ const urls = useAuthUrls();
209
488
 
210
- ## Development
489
+ <a href={urls.registerUrl().url}>Create account</a>
490
+ <a href={urls.recoverAccountUrl({ email: user.email }).url}>Forgot password?</a>
491
+ <a href={urls.selectAccountUrl().url}>Switch account</a>
492
+ ```
493
+
494
+ #### `useSessionStatus()` — Reactive status
495
+
496
+ ```tsx
497
+ const { isExpired, needsVerification, isGuest } = useSessionStatus();
498
+
499
+ {needsVerification && <Banner>Please verify your email</Banner>}
500
+ {isGuest && <button onClick={() => upgradeAccount()}>Upgrade account</button>}
501
+ ```
502
+
503
+ ### DouveryAuthProvider Props
504
+
505
+ ```typescript
506
+ interface DouveryAuthProviderProps {
507
+ config: DouveryAuthConfig;
508
+ children: ReactNode;
509
+ client?: DouveryAuthClient; // Optional pre-configured client
510
+ onAuthenticated?: (user: User) => void;
511
+ onLogout?: () => void;
512
+ onError?: (error: Error) => void;
513
+ }
514
+ ```
515
+
516
+ ---
517
+
518
+ ## ⚡ Qwik Hooks
519
+
520
+ | Hook | Return Type | Description |
521
+ |------|-------------|-------------|
522
+ | `useDouveryAuth()` | Context | Full context with signals and client |
523
+ | `useUser()` | `Signal<User \| null>` | Reactive user signal |
524
+ | `useIsAuthenticated()` | `Signal<boolean>` | Reactive auth status |
525
+ | `useAuthActions()` | All actions + `isLoading` | Auth actions (same as React) |
526
+ | `useAuthUrls()` | URL builders | Build URLs for auth pages |
527
+ | `useSessionStatus()` | `{ isExpired, needsVerification, isGuest }` | Reactive session signals |
528
+
529
+ #### Qwik example with navigation
530
+
531
+ ```tsx
532
+ import { component$ } from '@builder.io/qwik';
533
+ import { useAuthActions, useSessionStatus } from '@douvery/auth/qwik';
534
+
535
+ export const AccountMenu = component$(() => {
536
+ const { selectAccount, addAccount, recoverAccount, upgradeAccount } = useAuthActions();
537
+ const { isGuest, needsVerification } = useSessionStatus();
538
+
539
+ return (
540
+ <div>
541
+ <button onClick$={() => selectAccount({ returnTo: window.location.href })}>
542
+ Switch account
543
+ </button>
544
+ <button onClick$={() => addAccount()}>Add account</button>
545
+ {isGuest.value && (
546
+ <button onClick$={() => upgradeAccount()}>Upgrade account</button>
547
+ )}
548
+ {needsVerification.value && (
549
+ <button onClick$={() => verifyAccount()}>Verify email</button>
550
+ )}
551
+ </div>
552
+ );
553
+ });
554
+ ```
555
+
556
+ ---
557
+
558
+ ## 🔒 Security Best Practices
559
+
560
+ ### 1. Always Use HTTPS in Production
561
+ ```typescript
562
+ const config = {
563
+ redirectUri: 'https://yourapp.com/callback', // Not http://
564
+ };
565
+ ```
566
+
567
+ ### 2. Validate Redirect URIs
568
+ Register exact redirect URIs in your OAuth application settings.
569
+
570
+ ### 3. Use Appropriate Storage
571
+ ```typescript
572
+ // For high-security apps, use memory storage
573
+ const auth = createDouveryAuth({
574
+ storage: 'memory', // Tokens cleared on page refresh
575
+ });
576
+
577
+ // For normal apps, localStorage is fine
578
+ const auth = createDouveryAuth({
579
+ storage: 'localStorage', // Persists across tabs/sessions
580
+ });
581
+ ```
582
+
583
+ ### 4. Handle Token Expiry
584
+ ```typescript
585
+ auth.subscribe((event) => {
586
+ if (event.type === 'SESSION_EXPIRED') {
587
+ // Redirect to login or show re-auth modal
588
+ auth.login({ prompt: 'login' });
589
+ }
590
+ });
591
+ ```
592
+
593
+ ---
594
+
595
+ ## 🔧 Handling Callbacks
596
+
597
+ ### React
598
+
599
+ ```tsx
600
+ // pages/callback.tsx or routes/callback.tsx
601
+ import { useEffect } from 'react';
602
+ import { useDouveryAuth } from '@douvery/auth/react';
603
+ import { useNavigate } from 'react-router-dom';
604
+
605
+ export function CallbackPage() {
606
+ const { isInitialized, isAuthenticated, error } = useDouveryAuth();
607
+ const navigate = useNavigate();
608
+
609
+ useEffect(() => {
610
+ if (isInitialized) {
611
+ if (isAuthenticated) {
612
+ navigate('/dashboard');
613
+ } else if (error) {
614
+ navigate('/login?error=' + error.message);
615
+ }
616
+ }
617
+ }, [isInitialized, isAuthenticated, error, navigate]);
618
+
619
+ return <div>Completing login...</div>;
620
+ }
621
+ ```
622
+
623
+ ### Qwik
624
+
625
+ ```tsx
626
+ // routes/callback/index.tsx
627
+ import { component$ } from '@builder.io/qwik';
628
+ import { useNavigate } from '@builder.io/qwik-city';
629
+ import { useDouveryAuth } from '@douvery/auth/qwik';
630
+
631
+ export default component$(() => {
632
+ const { isInitialized, state, error } = useDouveryAuth();
633
+ const nav = useNavigate();
634
+
635
+ useVisibleTask$(({ track }) => {
636
+ track(() => isInitialized.value);
637
+ if (isInitialized.value) {
638
+ if (state.value.status === 'authenticated') {
639
+ nav('/dashboard');
640
+ } else if (error.value) {
641
+ nav('/login?error=' + error.value.message);
642
+ }
643
+ }
644
+ });
645
+
646
+ return <div>Completing login...</div>;
647
+ });
648
+ ```
649
+
650
+ ---
651
+
652
+ ## 🛠ïļ Advanced Usage
653
+
654
+ ### Custom Storage
655
+
656
+ ```typescript
657
+ import { createDouveryAuth, TokenStorage } from '@douvery/auth';
658
+
659
+ const secureStorage: TokenStorage = {
660
+ get: (key) => secureStore.getItem(key),
661
+ set: (key, value) => secureStore.setItem(key, value),
662
+ remove: (key) => secureStore.removeItem(key),
663
+ clear: () => secureStore.clear(),
664
+ };
665
+
666
+ const auth = createDouveryAuth({
667
+ clientId: 'your-client-id',
668
+ redirectUri: '/callback',
669
+ customStorage: secureStorage,
670
+ });
671
+ ```
672
+
673
+ ### Pre-configured Client (React)
674
+
675
+ ```tsx
676
+ import { DouveryAuthProvider } from '@douvery/auth/react';
677
+ import { createDouveryAuth } from '@douvery/auth';
678
+
679
+ // Create client once, outside component
680
+ const authClient = createDouveryAuth({
681
+ clientId: 'your-client-id',
682
+ redirectUri: '/callback',
683
+ });
684
+
685
+ function App() {
686
+ return (
687
+ <DouveryAuthProvider config={{}} client={authClient}>
688
+ <YourApp />
689
+ </DouveryAuthProvider>
690
+ );
691
+ }
692
+ ```
693
+
694
+ ### API Request with Token
695
+
696
+ ```typescript
697
+ async function fetchProtectedData() {
698
+ const token = await auth.getAccessToken();
699
+
700
+ if (!token) {
701
+ throw new Error('Not authenticated');
702
+ }
703
+
704
+ const response = await fetch('https://api.example.com/data', {
705
+ headers: {
706
+ Authorization: `Bearer ${token}`,
707
+ 'Content-Type': 'application/json',
708
+ },
709
+ });
710
+
711
+ if (response.status === 401) {
712
+ // Token might be invalid, try to refresh
713
+ await auth.refreshTokens();
714
+ return fetchProtectedData();
715
+ }
716
+
717
+ return response.json();
718
+ }
719
+ ```
720
+
721
+ ---
722
+
723
+ ## 📁 Package Structure
724
+
725
+ ```
726
+ @douvery/auth
727
+ ├── dist/
728
+ │ ├── index.js # Core (ESM)
729
+ │ ├── index.d.ts # Core types
730
+ │ ├── react/
731
+ │ │ ├── index.js # React adapter
732
+ │ │ └── index.d.ts # React types
733
+ │ └── qwik/
734
+ │ ├── index.js # Qwik adapter
735
+ │ └── index.d.ts # Qwik types
736
+ ```
737
+
738
+ **Imports:**
739
+ ```typescript
740
+ // Core
741
+ import { createDouveryAuth, DouveryAuthClient } from '@douvery/auth';
742
+
743
+ // React
744
+ import { DouveryAuthProvider, useDouveryAuth } from '@douvery/auth/react';
745
+
746
+ // Qwik
747
+ import { DouveryAuthProvider, useDouveryAuth } from '@douvery/auth/qwik';
748
+ ```
749
+
750
+ ---
751
+
752
+ ## ðŸĪ Contributing
211
753
 
212
754
  ```bash
755
+ # Clone the repository
756
+ git clone https://github.com/douvery/douvery-auth.git
757
+ cd douvery-auth
758
+
213
759
  # Install dependencies
214
- pnpm install
760
+ bun install
215
761
 
216
762
  # Build all packages
217
- pnpm build
763
+ npm run build
218
764
 
219
- # Run in development mode
220
- pnpm dev
765
+ # Run type checking
766
+ npx tsc --noEmit
767
+ ```
221
768
 
222
- # Type check
223
- pnpm typecheck
769
+ ---
224
770
 
225
- # Clean build outputs
226
- pnpm clean
227
- ```
771
+ ## 📄 License
772
+
773
+ MIT ÂĐ [Douvery](https://douvery.com)
228
774
 
229
- ## License
775
+ ---
230
776
 
231
- MIT
777
+ <p align="center">
778
+ Made with âĪïļ by the Douvery team
779
+ </p>