@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 +641 -93
- package/dist/index.d.ts +107 -1
- package/dist/index.js +187 -0
- package/dist/index.js.map +1 -1
- package/dist/qwik/index.d.ts +135 -1
- package/dist/qwik/index.js +296 -24
- package/dist/qwik/index.js.map +1 -1
- package/dist/react/index.d.ts +144 -1
- package/dist/react/index.js +373 -45
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,88 +1,117 @@
|
|
|
1
1
|
# @douvery/auth
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
40
|
+
```bash
|
|
41
|
+
# npm
|
|
42
|
+
npm install @douvery/auth
|
|
6
43
|
|
|
7
|
-
|
|
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
|
-
|
|
47
|
+
# bun
|
|
48
|
+
bun add @douvery/auth
|
|
14
49
|
|
|
15
|
-
|
|
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
|
-
###
|
|
54
|
+
### Peer Dependencies (Optional)
|
|
24
55
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
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: '
|
|
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
|
|
87
|
+
// 2. Use hooks in your components
|
|
63
88
|
function LoginButton() {
|
|
64
|
-
const { login, logout, isAuthenticated,
|
|
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
|
|
83
|
-
import { component
|
|
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
|
|
130
|
+
// 2. Use hooks in your components (signal-based)
|
|
102
131
|
export const LoginButton = component$(() => {
|
|
103
|
-
const
|
|
132
|
+
const user = useUser();
|
|
133
|
+
const { login, logout, isLoading } = useAuthActions();
|
|
104
134
|
|
|
105
135
|
return (
|
|
106
136
|
<>
|
|
107
|
-
{
|
|
137
|
+
{user.value ? (
|
|
108
138
|
<div>
|
|
109
|
-
<
|
|
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()}>
|
|
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: '
|
|
161
|
+
redirectUri: window.location.origin + '/callback',
|
|
128
162
|
issuer: 'https://auth.douvery.com',
|
|
129
163
|
scopes: ['openid', 'profile', 'email'],
|
|
130
164
|
});
|
|
131
165
|
|
|
132
|
-
//
|
|
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
|
-
//
|
|
136
|
-
|
|
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
|
-
//
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
169
|
-
|
|
256
|
+
/** Custom storage implementation */
|
|
257
|
+
customStorage?: TokenStorage;
|
|
170
258
|
|
|
171
|
-
/** Auto-refresh tokens before expiry
|
|
172
|
-
autoRefresh?: boolean;
|
|
259
|
+
/** Auto-refresh tokens before expiry */
|
|
260
|
+
autoRefresh?: boolean; // default: true
|
|
173
261
|
|
|
174
|
-
/** Seconds before expiry to trigger refresh
|
|
175
|
-
refreshThreshold?: number;
|
|
262
|
+
/** Seconds before expiry to trigger refresh */
|
|
263
|
+
refreshThreshold?: number; // default: 60
|
|
176
264
|
|
|
177
|
-
/** Enable debug logging
|
|
178
|
-
debug?: boolean;
|
|
265
|
+
/** Enable debug logging */
|
|
266
|
+
debug?: boolean; // default: false
|
|
179
267
|
}
|
|
180
268
|
```
|
|
181
269
|
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
760
|
+
bun install
|
|
215
761
|
|
|
216
762
|
# Build all packages
|
|
217
|
-
|
|
763
|
+
npm run build
|
|
218
764
|
|
|
219
|
-
# Run
|
|
220
|
-
|
|
765
|
+
# Run type checking
|
|
766
|
+
npx tsc --noEmit
|
|
767
|
+
```
|
|
221
768
|
|
|
222
|
-
|
|
223
|
-
pnpm typecheck
|
|
769
|
+
---
|
|
224
770
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
771
|
+
## ð License
|
|
772
|
+
|
|
773
|
+
MIT ÂĐ [Douvery](https://douvery.com)
|
|
228
774
|
|
|
229
|
-
|
|
775
|
+
---
|
|
230
776
|
|
|
231
|
-
|
|
777
|
+
<p align="center">
|
|
778
|
+
Made with âĪïļ by the Douvery team
|
|
779
|
+
</p>
|