@netlify/identity 0.1.1-alpha.2 → 0.1.1-alpha.21

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
@@ -19,18 +19,20 @@ npm install @netlify/identity
19
19
 
20
20
  ## Quick start
21
21
 
22
- ### Browser
22
+ ### Log in (browser)
23
23
 
24
24
  ```ts
25
- import { getUser } from '@netlify/identity'
25
+ import { login, getUser } from '@netlify/identity'
26
26
 
27
- const user = getUser()
28
- if (user) {
29
- console.log(`Hello, ${user.name}`)
30
- }
27
+ // Log in
28
+ const user = await login('jane@example.com', 'password123')
29
+ console.log(`Hello, ${user.name}`)
30
+
31
+ // Later, check auth state synchronously
32
+ const currentUser = getUser()
31
33
  ```
32
34
 
33
- ### Netlify Function
35
+ ### Protect a Netlify Function
34
36
 
35
37
  ```ts
36
38
  import { getUser } from '@netlify/identity'
@@ -43,7 +45,7 @@ export default async (req: Request, context: Context) => {
43
45
  }
44
46
  ```
45
47
 
46
- ### Edge Function
48
+ ### Protect an Edge Function
47
49
 
48
50
  ```ts
49
51
  import { getUser } from '@netlify/identity'
@@ -66,7 +68,9 @@ export default async (req: Request, context: Context) => {
66
68
  getUser(): User | null
67
69
  ```
68
70
 
69
- Returns the current authenticated user, or `null` if not logged in. Synchronous, never throws.
71
+ Returns the current authenticated user, or `null` if not logged in. Synchronous. Never throws.
72
+
73
+ > **Next.js note:** Calling `getUser()` in a Server Component opts the page into [dynamic rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-rendering) because it reads cookies. This is expected and correct for authenticated pages. Next.js handles the internal dynamic rendering signal automatically.
70
74
 
71
75
  #### `isAuthenticated`
72
76
 
@@ -74,7 +78,7 @@ Returns the current authenticated user, or `null` if not logged in. Synchronous,
74
78
  isAuthenticated(): boolean
75
79
  ```
76
80
 
77
- Returns `true` if a user is currently authenticated. Equivalent to `getUser() !== null`.
81
+ Returns `true` if a user is currently authenticated. Equivalent to `getUser() !== null`. Never throws.
78
82
 
79
83
  #### `getIdentityConfig`
80
84
 
@@ -90,7 +94,9 @@ Returns the Identity endpoint URL (and operator token on the server), or `null`
90
94
  getSettings(): Promise<Settings>
91
95
  ```
92
96
 
93
- Fetches your project's Identity settings (enabled providers, autoconfirm, signup disabled). Throws `MissingIdentityError` if not configured; throws `AuthError` if the endpoint is unreachable.
97
+ Fetches your project's Identity settings (enabled providers, autoconfirm, signup disabled).
98
+
99
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the endpoint is unreachable.
94
100
 
95
101
  #### `login`
96
102
 
@@ -98,15 +104,25 @@ Fetches your project's Identity settings (enabled providers, autoconfirm, signup
98
104
  login(email: string, password: string): Promise<User>
99
105
  ```
100
106
 
101
- Logs in with email and password. Browser only.
107
+ Logs in with email and password. Works in both browser and server contexts.
108
+
109
+ In the browser, uses gotrue-js and emits a `'login'` event. On the server (Netlify Functions, Edge Functions), calls the GoTrue HTTP API directly and sets the `nf_jwt` cookie via the Netlify runtime.
110
+
111
+ **Throws:** `AuthError` on invalid credentials or network failure. In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
102
112
 
103
113
  #### `signup`
104
114
 
105
115
  ```ts
106
- signup(email: string, password: string, data?: Record<string, unknown>): Promise<User>
116
+ signup(email: string, password: string, data?: SignupData): Promise<User>
107
117
  ```
108
118
 
109
- Creates a new account. Emits `'login'` if autoconfirm is enabled. Browser only.
119
+ Creates a new account. Works in both browser and server contexts.
120
+
121
+ If autoconfirm is enabled in your Identity settings, the user is logged in immediately: cookies are set and a `'login'` event is emitted. If autoconfirm is **disabled** (the default), the user receives a confirmation email and must click the link before they can log in. In that case, no cookies are set and no auth event is emitted.
122
+
123
+ The optional `data` parameter sets user metadata (e.g., `{ full_name: 'Jane Doe' }`), stored in the user's `user_metadata` field.
124
+
125
+ **Throws:** `AuthError` on failure (e.g., email already registered, signup disabled). In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
110
126
 
111
127
  #### `logout`
112
128
 
@@ -114,7 +130,11 @@ Creates a new account. Emits `'login'` if autoconfirm is enabled. Browser only.
114
130
  logout(): Promise<void>
115
131
  ```
116
132
 
117
- Logs out the current user and clears the session. Browser only.
133
+ Logs out the current user and clears the session. Works in both browser and server contexts.
134
+
135
+ In the browser, uses gotrue-js and emits a `'logout'` event. On the server, calls GoTrue's `/logout` endpoint with the JWT from the `nf_jwt` cookie, then deletes the cookie. Auth cookies are always cleared, even if the GoTrue call fails.
136
+
137
+ **Throws:** In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
118
138
 
119
139
  #### `oauthLogin`
120
140
 
@@ -122,7 +142,11 @@ Logs out the current user and clears the session. Browser only.
122
142
  oauthLogin(provider: string): never
123
143
  ```
124
144
 
125
- Redirects to an OAuth provider. Always throws (the page navigates away). Browser only.
145
+ Redirects to an OAuth provider. The page navigates away, so this function never returns normally. Browser only.
146
+
147
+ The `provider` argument should be one of the `AuthProvider` values: `'google'`, `'github'`, `'gitlab'`, `'bitbucket'`, `'facebook'`, or `'saml'`.
148
+
149
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if called on the server.
126
150
 
127
151
  #### `handleAuthCallback`
128
152
 
@@ -132,6 +156,8 @@ handleAuthCallback(): Promise<CallbackResult | null>
132
156
 
133
157
  Processes the URL hash after an OAuth redirect, email confirmation, password recovery, invite acceptance, or email change. Call on page load. Returns `null` if the hash contains no auth parameters. Browser only.
134
158
 
159
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if token exchange fails.
160
+
135
161
  #### `onAuthChange`
136
162
 
137
163
  ```ts
@@ -140,6 +166,28 @@ onAuthChange(callback: AuthCallback): () => void
140
166
 
141
167
  Subscribes to auth state changes (login, logout, token refresh, user updates). Returns an unsubscribe function. Also fires on cross-tab session changes. No-op on the server.
142
168
 
169
+ #### `hydrateSession`
170
+
171
+ ```ts
172
+ hydrateSession(): Promise<User | null>
173
+ ```
174
+
175
+ Bootstraps the browser-side gotrue-js session from server-set auth cookies (`nf_jwt`, `nf_refresh`). Returns the hydrated `User`, or `null` if no auth cookies are present. No-op on the server.
176
+
177
+ **When to use:** After a server-side login (e.g., via a Netlify Function or Server Action), the `nf_jwt` cookie is set but gotrue-js has no browser session yet. `getUser()` works immediately (it decodes the cookie), but account operations like `updateUser()` or `verifyEmailChange()` require a live gotrue-js session. Call `hydrateSession()` once on page load to bridge this gap.
178
+
179
+ If a gotrue-js session already exists (e.g., from a browser-side login), this is a no-op and returns the existing user.
180
+
181
+ ```ts
182
+ import { hydrateSession, updateUser } from '@netlify/identity'
183
+
184
+ // On page load, hydrate the session from server-set cookies
185
+ await hydrateSession()
186
+
187
+ // Now browser account operations work
188
+ await updateUser({ data: { full_name: 'Jane Doe' } })
189
+ ```
190
+
143
191
  #### `requestPasswordRecovery`
144
192
 
145
193
  ```ts
@@ -148,6 +196,8 @@ requestPasswordRecovery(email: string): Promise<void>
148
196
 
149
197
  Sends a password recovery email to the given address.
150
198
 
199
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` on network failure.
200
+
151
201
  #### `confirmEmail`
152
202
 
153
203
  ```ts
@@ -156,6 +206,8 @@ confirmEmail(token: string): Promise<User>
156
206
 
157
207
  Confirms an email address using the token from a confirmation email. Logs the user in on success.
158
208
 
209
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the token is invalid or expired.
210
+
159
211
  #### `acceptInvite`
160
212
 
161
213
  ```ts
@@ -164,21 +216,37 @@ acceptInvite(token: string, password: string): Promise<User>
164
216
 
165
217
  Accepts an invite token and sets a password for the new account. Logs the user in on success.
166
218
 
219
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the token is invalid or expired.
220
+
167
221
  #### `verifyEmailChange`
168
222
 
169
223
  ```ts
170
224
  verifyEmailChange(token: string): Promise<User>
171
225
  ```
172
226
 
173
- Verifies an email change using the token from a verification email. Requires an active session.
227
+ Verifies an email change using the token from a verification email.
228
+
229
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the token is invalid.
230
+
231
+ #### `recoverPassword`
232
+
233
+ ```ts
234
+ recoverPassword(token: string, newPassword: string): Promise<User>
235
+ ```
236
+
237
+ Redeems a recovery token and sets a new password. Logs the user in on success.
238
+
239
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the token is invalid or expired.
174
240
 
175
241
  #### `updateUser`
176
242
 
177
243
  ```ts
178
- updateUser(updates: Record<string, unknown>): Promise<User>
244
+ updateUser(updates: UserUpdates): Promise<User>
179
245
  ```
180
246
 
181
- Updates the current user's metadata or credentials. Requires an active session.
247
+ Updates the current user's metadata or credentials. Requires an active session. Pass `email` or `password` to change credentials, or `data` to update user metadata (e.g., `{ data: { full_name: 'New Name' } }`).
248
+
249
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if no user is logged in, or the update fails.
182
250
 
183
251
  ### Types
184
252
 
@@ -224,6 +292,27 @@ interface IdentityConfig {
224
292
  type AuthProvider = 'google' | 'github' | 'gitlab' | 'bitbucket' | 'facebook' | 'saml' | 'email'
225
293
  ```
226
294
 
295
+ #### `UserUpdates`
296
+
297
+ ```ts
298
+ interface UserUpdates {
299
+ email?: string
300
+ password?: string
301
+ data?: Record<string, unknown>
302
+ [key: string]: unknown
303
+ }
304
+ ```
305
+
306
+ Fields accepted by `updateUser()`. All fields are optional.
307
+
308
+ #### `SignupData`
309
+
310
+ ```ts
311
+ type SignupData = Record<string, unknown>
312
+ ```
313
+
314
+ User metadata passed as the third argument to `signup()`. Stored in the user's `user_metadata` field.
315
+
227
316
  #### `AppMetadata`
228
317
 
229
318
  ```ts
@@ -256,6 +345,10 @@ interface CallbackResult {
256
345
  }
257
346
  ```
258
347
 
348
+ The `token` field is only present for `invite` callbacks, where the user hasn't set a password yet. Pass `token` to `acceptInvite(token, password)` to finish.
349
+
350
+ For all other types (`oauth`, `confirmation`, `recovery`, `email_change`), the user is logged in directly and `token` is not set.
351
+
259
352
  ### Errors
260
353
 
261
354
  #### `AuthError`
@@ -275,8 +368,369 @@ class MissingIdentityError extends Error {}
275
368
 
276
369
  Thrown when Identity is not configured in the current environment.
277
370
 
371
+ ## Framework integration
372
+
373
+ ### Recommended pattern for SSR frameworks
374
+
375
+ For SSR frameworks (Next.js, Remix, Astro, TanStack Start), the recommended pattern is:
376
+
377
+ - **Browser-side** for auth mutations: `login()`, `signup()`, `logout()`, `oauthLogin()`
378
+ - **Server-side** for reading auth state: `getUser()`, `getSettings()`, `getIdentityConfig()`
379
+
380
+ Browser-side auth mutations call the GoTrue API directly from the browser, set the `nf_jwt` cookie and gotrue-js localStorage, and emit `onAuthChange` events. This keeps the client UI in sync immediately. Server-side reads work because the cookie is sent with every request.
381
+
382
+ The library also supports server-side mutations (`login()`, `signup()`, `logout()` inside Netlify Functions), but these require the Netlify Functions runtime to set cookies. After a server-side mutation, you need a full page navigation so the browser sends the new cookie.
383
+
384
+ ### Next.js (App Router)
385
+
386
+ **Server Actions return results; the client handles navigation:**
387
+
388
+ ```tsx
389
+ // app/actions.ts
390
+ 'use server'
391
+ import { login, logout } from '@netlify/identity'
392
+
393
+ export async function loginAction(formData: FormData) {
394
+ const email = formData.get('email') as string
395
+ const password = formData.get('password') as string
396
+ await login(email, password)
397
+ return { success: true }
398
+ }
399
+
400
+ export async function logoutAction() {
401
+ await logout()
402
+ return { success: true }
403
+ }
404
+ ```
405
+
406
+ ```tsx
407
+ // app/login/page.tsx
408
+ 'use client'
409
+ import { loginAction } from '../actions'
410
+
411
+ export default function LoginPage() {
412
+ async function handleSubmit(formData: FormData) {
413
+ const result = await loginAction(formData)
414
+ if (result.success) {
415
+ window.location.href = '/dashboard' // full page load
416
+ }
417
+ }
418
+
419
+ return <form action={handleSubmit}>...</form>
420
+ }
421
+ ```
422
+
423
+ ```tsx
424
+ // app/dashboard/page.tsx
425
+ import { getUser } from '@netlify/identity'
426
+ import { redirect } from 'next/navigation'
427
+
428
+ export default function Dashboard() {
429
+ const user = getUser()
430
+ if (!user) redirect('/login')
431
+
432
+ return <h1>Hello, {user.email}</h1>
433
+ }
434
+ ```
435
+
436
+ Use `window.location.href` instead of Next.js `redirect()` after server-side auth mutations. Next.js `redirect()` triggers a soft navigation via the Router, which may not include the newly-set auth cookie. A full page load ensures the cookie is sent and the server sees the updated auth state. Reading auth state with `getUser()` in Server Components works normally, and `redirect()` is fine for auth gates (where no cookie was just set).
437
+
438
+ ### Remix
439
+
440
+ **Login with Action (server-side pattern):**
441
+
442
+ ```tsx
443
+ // app/routes/login.tsx
444
+ import { login } from '@netlify/identity'
445
+ import { redirect, json } from '@remix-run/node'
446
+ import type { ActionFunctionArgs } from '@remix-run/node'
447
+
448
+ export async function action({ request }: ActionFunctionArgs) {
449
+ const formData = await request.formData()
450
+ const email = formData.get('email') as string
451
+ const password = formData.get('password') as string
452
+
453
+ try {
454
+ await login(email, password)
455
+ return redirect('/dashboard')
456
+ } catch (error) {
457
+ return json({ error: (error as Error).message }, { status: 400 })
458
+ }
459
+ }
460
+ ```
461
+
462
+ ```tsx
463
+ // app/routes/dashboard.tsx
464
+ import { getUser } from '@netlify/identity'
465
+ import { redirect } from '@remix-run/node'
466
+
467
+ export async function loader() {
468
+ const user = getUser()
469
+ if (!user) return redirect('/login')
470
+ return { user }
471
+ }
472
+ ```
473
+
474
+ Remix `redirect()` works after server-side `login()` because Remix actions return real HTTP responses. The browser receives a 302 with the `Set-Cookie` header already applied, so the next request includes the auth cookie. This is different from Next.js, where `redirect()` in a Server Action triggers a client-side (soft) navigation that may not include newly-set cookies.
475
+
476
+ ### TanStack Start
477
+
478
+ **Login from the browser (recommended):**
479
+
480
+ ```tsx
481
+ // app/server/auth.ts - server functions for reads only
482
+ import { createServerFn } from '@tanstack/react-start'
483
+ import { getUser } from '@netlify/identity'
484
+
485
+ export const getServerUser = createServerFn({ method: 'GET' }).handler(async () => {
486
+ const user = getUser()
487
+ return user ?? null
488
+ })
489
+ ```
490
+
491
+ ```tsx
492
+ // app/routes/login.tsx - browser-side auth for mutations
493
+ import { login, signup, onAuthChange } from '@netlify/identity'
494
+ import { getServerUser } from '~/server/auth'
495
+
496
+ export const Route = createFileRoute('/login')({
497
+ beforeLoad: async () => {
498
+ const user = await getServerUser()
499
+ if (user) throw redirect({ to: '/dashboard' })
500
+ },
501
+ component: Login,
502
+ })
503
+
504
+ function Login() {
505
+ const handleLogin = async (email: string, password: string) => {
506
+ await login(email, password) // browser-side: sets cookie + localStorage
507
+ window.location.href = '/dashboard'
508
+ }
509
+ // ...
510
+ }
511
+ ```
512
+
513
+ ```tsx
514
+ // app/routes/dashboard.tsx
515
+ import { logout } from '@netlify/identity'
516
+ import { getServerUser } from '~/server/auth'
517
+
518
+ export const Route = createFileRoute('/dashboard')({
519
+ beforeLoad: async () => {
520
+ const user = await getServerUser()
521
+ if (!user) throw redirect({ to: '/login' })
522
+ },
523
+ loader: async () => {
524
+ const user = await getServerUser()
525
+ return { user: user! }
526
+ },
527
+ component: Dashboard,
528
+ })
529
+
530
+ function Dashboard() {
531
+ const { user } = Route.useLoaderData()
532
+
533
+ const handleLogout = async () => {
534
+ await logout() // browser-side: clears cookie + localStorage
535
+ window.location.href = '/'
536
+ }
537
+ // ...
538
+ }
539
+ ```
540
+
541
+ Use `window.location.href` instead of TanStack Router's `navigate()` after auth changes. This ensures the browser sends the updated cookie on the next request.
542
+
543
+ ### Astro (SSR)
544
+
545
+ **Login via API endpoint (server-side pattern):**
546
+
547
+ ```ts
548
+ // src/pages/api/login.ts
549
+ import type { APIRoute } from 'astro'
550
+ import { login } from '@netlify/identity'
551
+
552
+ export const POST: APIRoute = async ({ request }) => {
553
+ const { email, password } = await request.json()
554
+
555
+ try {
556
+ await login(email, password)
557
+ return new Response(null, {
558
+ status: 302,
559
+ headers: { Location: '/dashboard' },
560
+ })
561
+ } catch (error) {
562
+ return Response.json({ error: (error as Error).message }, { status: 400 })
563
+ }
564
+ }
565
+ ```
566
+
567
+ ```astro
568
+ ---
569
+ // src/pages/dashboard.astro
570
+ import { getUser } from '@netlify/identity'
571
+
572
+ const user = getUser()
573
+ if (!user) return Astro.redirect('/login')
574
+ ---
575
+ <h1>Hello, {user.email}</h1>
576
+ ```
577
+
578
+ ### SvelteKit
579
+
580
+ **Login from the browser (recommended):**
581
+
582
+ ```svelte
583
+ <!-- src/routes/login/+page.svelte -->
584
+ <script lang="ts">
585
+ import { login } from '@netlify/identity'
586
+
587
+ let email = ''
588
+ let password = ''
589
+ let error = ''
590
+
591
+ async function handleLogin() {
592
+ try {
593
+ await login(email, password)
594
+ window.location.href = '/dashboard'
595
+ } catch (e) {
596
+ error = (e as Error).message
597
+ }
598
+ }
599
+ </script>
600
+
601
+ <form on:submit|preventDefault={handleLogin}>
602
+ <input bind:value={email} type="email" />
603
+ <input bind:value={password} type="password" />
604
+ <button type="submit">Log in</button>
605
+ {#if error}<p>{error}</p>{/if}
606
+ </form>
607
+ ```
608
+
609
+ ```ts
610
+ // src/routes/dashboard/+page.server.ts
611
+ import { getUser } from '@netlify/identity'
612
+ import { redirect } from '@sveltejs/kit'
613
+
614
+ export function load() {
615
+ const user = getUser()
616
+ if (!user) redirect(302, '/login')
617
+ return { user }
618
+ }
619
+ ```
620
+
621
+ ### Handling OAuth callbacks in SPAs
622
+
623
+ All SPA frameworks need a callback handler that runs on page load to process OAuth redirects, email confirmations, and password recovery tokens.
624
+
625
+ ```tsx
626
+ // React component (works with Next.js, Remix, TanStack Start)
627
+ import { useEffect } from 'react'
628
+ import { handleAuthCallback } from '@netlify/identity'
629
+
630
+ export function CallbackHandler() {
631
+ useEffect(() => {
632
+ if (!window.location.hash) return
633
+
634
+ handleAuthCallback().then((result) => {
635
+ if (!result) return
636
+ if (result.type === 'invite') {
637
+ window.location.href = `/accept-invite?token=${result.token}`
638
+ } else {
639
+ window.location.href = '/dashboard'
640
+ }
641
+ })
642
+ }, [])
643
+
644
+ return null
645
+ }
646
+ ```
647
+
648
+ Mount this component in your **root layout** so it runs on every page. If you only mount it on a `/callback` route, OAuth redirects and email confirmation links that land on other pages will not be processed.
649
+
278
650
  ## Guides
279
651
 
652
+ ### React `useAuth` hook
653
+
654
+ The library is framework-agnostic, but here's a simple React hook for keeping components in sync with auth state:
655
+
656
+ ```tsx
657
+ import { useState, useEffect } from 'react'
658
+ import { getUser, onAuthChange } from '@netlify/identity'
659
+ import type { User } from '@netlify/identity'
660
+
661
+ export function useAuth() {
662
+ const [user, setUser] = useState<User | null>(getUser())
663
+
664
+ useEffect(() => {
665
+ return onAuthChange((_event, user) => setUser(user))
666
+ }, [])
667
+
668
+ return user
669
+ }
670
+ ```
671
+
672
+ ```tsx
673
+ function NavBar() {
674
+ const user = useAuth()
675
+ return user ? <p>Hello, {user.name}</p> : <a href="/login">Log in</a>
676
+ }
677
+ ```
678
+
679
+ ### Listening for auth changes
680
+
681
+ Use `onAuthChange` to keep your UI in sync with auth state. It fires on login, logout, token refresh, and user updates. It also detects session changes in other browser tabs (via `localStorage`).
682
+
683
+ ```ts
684
+ import { onAuthChange } from '@netlify/identity'
685
+
686
+ const unsubscribe = onAuthChange((event, user) => {
687
+ switch (event) {
688
+ case 'login':
689
+ console.log('Logged in:', user?.email)
690
+ break
691
+ case 'logout':
692
+ console.log('Logged out')
693
+ break
694
+ case 'token_refresh':
695
+ console.log('Token refreshed for:', user?.email)
696
+ break
697
+ case 'user_updated':
698
+ console.log('User updated:', user?.email)
699
+ break
700
+ }
701
+ })
702
+
703
+ // Later, to stop listening:
704
+ unsubscribe()
705
+ ```
706
+
707
+ On the server, `onAuthChange` is a no-op and the returned unsubscribe function does nothing.
708
+
709
+ ### OAuth login
710
+
711
+ OAuth login is a two-step flow: redirect the user to the provider, then process the callback when they return.
712
+
713
+ **Step by step:**
714
+
715
+ ```ts
716
+ import { oauthLogin, handleAuthCallback } from '@netlify/identity'
717
+
718
+ // 1. Kick off the OAuth flow (e.g., from a "Sign in with GitHub" button).
719
+ // This navigates away from the page and does not return.
720
+ oauthLogin('github')
721
+ ```
722
+
723
+ ```ts
724
+ // 2. On page load, handle the redirect back from the provider.
725
+ const result = await handleAuthCallback()
726
+
727
+ if (result?.type === 'oauth') {
728
+ console.log('Logged in via OAuth:', result.user?.email)
729
+ }
730
+ ```
731
+
732
+ `handleAuthCallback()` exchanges the token in the URL hash, logs the user in, clears the hash, and emits a `'login'` event via `onAuthChange`.
733
+
280
734
  ### Password recovery
281
735
 
282
736
  Password recovery is a two-step flow. The library handles the token exchange automatically via `handleAuthCallback()`, which logs the user in and returns `{type: 'recovery', user}`. You then show a "set new password" form and call `updateUser()` to save it.
@@ -300,6 +754,38 @@ if (result?.type === 'recovery') {
300
754
  }
301
755
  ```
302
756
 
757
+ ### Invite acceptance
758
+
759
+ When an admin invites a user, they receive an email with an invite link. Clicking it redirects to your site with an `invite_token` in the URL hash. Unlike other callback types, the user is not logged in automatically because they need to set a password first.
760
+
761
+ **Step by step:**
762
+
763
+ ```ts
764
+ import { handleAuthCallback, acceptInvite } from '@netlify/identity'
765
+
766
+ // 1. On page load, handle the callback.
767
+ const result = await handleAuthCallback()
768
+
769
+ if (result?.type === 'invite' && result.token) {
770
+ // 2. The user is NOT logged in yet. Show a "set your password" form.
771
+ // When they submit:
772
+ const password = document.getElementById('password').value
773
+ const user = await acceptInvite(result.token, password)
774
+ console.log('Account created:', user.email)
775
+ }
776
+ ```
777
+
778
+ ### Session lifetime
779
+
780
+ Sessions are managed by Netlify Identity (GoTrue) on the server side. The library stores two cookies:
781
+
782
+ - **`nf_jwt`**: A short-lived JWT access token (default: 1 hour). Automatically refreshed by gotrue-js in the browser using the refresh token.
783
+ - **`nf_refresh`**: A long-lived refresh token used to obtain new access tokens without re-authenticating.
784
+
785
+ In the browser, gotrue-js handles token refresh automatically in the background. On the server, the access token in the `nf_jwt` cookie is validated as-is; if it has expired, `getUser()` returns `null`. The user will need to refresh the page (which triggers a browser-side token refresh) or log in again.
786
+
787
+ Session lifetime is configured in your GoTrue/Identity server settings, not in this library.
788
+
303
789
  ## License
304
790
 
305
791
  MIT