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

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
@@ -74,7 +74,7 @@ Returns the current authenticated user, or `null` if not logged in. Synchronous,
74
74
  isAuthenticated(): boolean
75
75
  ```
76
76
 
77
- Returns `true` if a user is currently authenticated. Equivalent to `getUser() !== null`.
77
+ Returns `true` if a user is currently authenticated. Equivalent to `getUser() !== null`. Never throws.
78
78
 
79
79
  #### `getIdentityConfig`
80
80
 
@@ -90,7 +90,9 @@ Returns the Identity endpoint URL (and operator token on the server), or `null`
90
90
  getSettings(): Promise<Settings>
91
91
  ```
92
92
 
93
- Fetches your project's Identity settings (enabled providers, autoconfirm, signup disabled). Throws `MissingIdentityError` if not configured; throws `AuthError` if the endpoint is unreachable.
93
+ Fetches your project's Identity settings (enabled providers, autoconfirm, signup disabled).
94
+
95
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the endpoint is unreachable.
94
96
 
95
97
  #### `login`
96
98
 
@@ -98,7 +100,11 @@ Fetches your project's Identity settings (enabled providers, autoconfirm, signup
98
100
  login(email: string, password: string): Promise<User>
99
101
  ```
100
102
 
101
- Logs in with email and password. Browser only.
103
+ Logs in with email and password. Works in both browser and server contexts.
104
+
105
+ 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.
106
+
107
+ **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
108
 
103
109
  #### `signup`
104
110
 
@@ -106,7 +112,11 @@ Logs in with email and password. Browser only.
106
112
  signup(email: string, password: string, data?: Record<string, unknown>): Promise<User>
107
113
  ```
108
114
 
109
- Creates a new account. Emits `'login'` if autoconfirm is enabled. Browser only.
115
+ Creates a new account. Works in both browser and server contexts.
116
+
117
+ In the browser, uses gotrue-js and emits `'login'` if autoconfirm is enabled. On the server, calls the GoTrue HTTP API directly and sets the `nf_jwt` cookie if the user is auto-confirmed.
118
+
119
+ **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
120
 
111
121
  #### `logout`
112
122
 
@@ -114,7 +124,11 @@ Creates a new account. Emits `'login'` if autoconfirm is enabled. Browser only.
114
124
  logout(): Promise<void>
115
125
  ```
116
126
 
117
- Logs out the current user and clears the session. Browser only.
127
+ Logs out the current user and clears the session. Works in both browser and server contexts.
128
+
129
+ 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.
130
+
131
+ **Throws:** `AuthError` on network failure. In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
118
132
 
119
133
  #### `oauthLogin`
120
134
 
@@ -122,7 +136,11 @@ Logs out the current user and clears the session. Browser only.
122
136
  oauthLogin(provider: string): never
123
137
  ```
124
138
 
125
- Redirects to an OAuth provider. Always throws (the page navigates away). Browser only.
139
+ Redirects to an OAuth provider. The page navigates away, so this function never returns normally. Browser only.
140
+
141
+ The `provider` argument should be one of the `AuthProvider` values: `'google'`, `'github'`, `'gitlab'`, `'bitbucket'`, `'facebook'`, or `'saml'`.
142
+
143
+ **Throws:** `MissingIdentityError` if Identity is not configured. `Error` if called on the server.
126
144
 
127
145
  #### `handleAuthCallback`
128
146
 
@@ -132,6 +150,8 @@ handleAuthCallback(): Promise<CallbackResult | null>
132
150
 
133
151
  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
152
 
153
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if token exchange fails.
154
+
135
155
  #### `onAuthChange`
136
156
 
137
157
  ```ts
@@ -148,6 +168,8 @@ requestPasswordRecovery(email: string): Promise<void>
148
168
 
149
169
  Sends a password recovery email to the given address.
150
170
 
171
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` on network failure.
172
+
151
173
  #### `confirmEmail`
152
174
 
153
175
  ```ts
@@ -156,6 +178,8 @@ confirmEmail(token: string): Promise<User>
156
178
 
157
179
  Confirms an email address using the token from a confirmation email. Logs the user in on success.
158
180
 
181
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the token is invalid or expired.
182
+
159
183
  #### `acceptInvite`
160
184
 
161
185
  ```ts
@@ -164,13 +188,27 @@ acceptInvite(token: string, password: string): Promise<User>
164
188
 
165
189
  Accepts an invite token and sets a password for the new account. Logs the user in on success.
166
190
 
191
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the token is invalid or expired.
192
+
167
193
  #### `verifyEmailChange`
168
194
 
169
195
  ```ts
170
196
  verifyEmailChange(token: string): Promise<User>
171
197
  ```
172
198
 
173
- Verifies an email change using the token from a verification email. Requires an active session.
199
+ Verifies an email change using the token from a verification email.
200
+
201
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the token is invalid.
202
+
203
+ #### `recoverPassword`
204
+
205
+ ```ts
206
+ recoverPassword(token: string, newPassword: string): Promise<User>
207
+ ```
208
+
209
+ Redeems a recovery token and sets a new password. Logs the user in on success.
210
+
211
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the token is invalid or expired.
174
212
 
175
213
  #### `updateUser`
176
214
 
@@ -180,6 +218,8 @@ updateUser(updates: Record<string, unknown>): Promise<User>
180
218
 
181
219
  Updates the current user's metadata or credentials. Requires an active session.
182
220
 
221
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if no user is logged in, or the update fails.
222
+
183
223
  ### Types
184
224
 
185
225
  #### `User`
@@ -256,6 +296,10 @@ interface CallbackResult {
256
296
  }
257
297
  ```
258
298
 
299
+ 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.
300
+
301
+ For all other types (`oauth`, `confirmation`, `recovery`, `email_change`), the user is logged in directly and `token` is not set.
302
+
259
303
  ### Errors
260
304
 
261
305
  #### `AuthError`
@@ -275,8 +319,299 @@ class MissingIdentityError extends Error {}
275
319
 
276
320
  Thrown when Identity is not configured in the current environment.
277
321
 
322
+ ## Framework integration
323
+
324
+ ### Recommended pattern for SSR frameworks
325
+
326
+ For SSR frameworks (Next.js, Remix, Astro, TanStack Start), the recommended pattern is:
327
+
328
+ - **Browser-side** for auth mutations: `login()`, `signup()`, `logout()`, `oauthLogin()`
329
+ - **Server-side** for reading auth state: `getUser()`, `getSettings()`, `getIdentityConfig()`
330
+
331
+ 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.
332
+
333
+ 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.
334
+
335
+ ### Next.js (App Router)
336
+
337
+ **Server Actions return results; the client handles navigation:**
338
+
339
+ ```tsx
340
+ // app/actions.ts
341
+ 'use server'
342
+ import { login, logout } from '@netlify/identity'
343
+
344
+ export async function loginAction(formData: FormData) {
345
+ const email = formData.get('email') as string
346
+ const password = formData.get('password') as string
347
+ await login(email, password)
348
+ return { success: true }
349
+ }
350
+
351
+ export async function logoutAction() {
352
+ await logout()
353
+ return { success: true }
354
+ }
355
+ ```
356
+
357
+ ```tsx
358
+ // app/login/page.tsx
359
+ 'use client'
360
+ import { loginAction } from '../actions'
361
+
362
+ export default function LoginPage() {
363
+ async function handleSubmit(formData: FormData) {
364
+ const result = await loginAction(formData)
365
+ if (result.success) {
366
+ window.location.href = '/dashboard' // full page load
367
+ }
368
+ }
369
+
370
+ return <form action={handleSubmit}>...</form>
371
+ }
372
+ ```
373
+
374
+ ```tsx
375
+ // app/dashboard/page.tsx
376
+ import { getUser } from '@netlify/identity'
377
+ import { redirect } from 'next/navigation'
378
+
379
+ export default function Dashboard() {
380
+ const user = getUser()
381
+ if (!user) redirect('/login')
382
+
383
+ return <h1>Hello, {user.email}</h1>
384
+ }
385
+ ```
386
+
387
+ 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).
388
+
389
+ ### Remix
390
+
391
+ **Login with Action (server-side pattern):**
392
+
393
+ ```tsx
394
+ // app/routes/login.tsx
395
+ import { login } from '@netlify/identity'
396
+ import { redirect, json } from '@remix-run/node'
397
+ import type { ActionFunctionArgs } from '@remix-run/node'
398
+
399
+ export async function action({ request }: ActionFunctionArgs) {
400
+ const formData = await request.formData()
401
+ const email = formData.get('email') as string
402
+ const password = formData.get('password') as string
403
+
404
+ try {
405
+ await login(email, password)
406
+ return redirect('/dashboard')
407
+ } catch (error) {
408
+ return json({ error: (error as Error).message }, { status: 400 })
409
+ }
410
+ }
411
+ ```
412
+
413
+ ```tsx
414
+ // app/routes/dashboard.tsx
415
+ import { getUser } from '@netlify/identity'
416
+ import { redirect } from '@remix-run/node'
417
+
418
+ export async function loader() {
419
+ const user = getUser()
420
+ if (!user) return redirect('/login')
421
+ return { user }
422
+ }
423
+ ```
424
+
425
+ Remix actions return HTTP responses, so `redirect()` after server-side `login()` works correctly with cookies.
426
+
427
+ ### TanStack Start
428
+
429
+ **Login from the browser (recommended):**
430
+
431
+ ```tsx
432
+ // app/server/auth.ts - server functions for reads only
433
+ import { createServerFn } from '@tanstack/react-start'
434
+ import { getUser } from '@netlify/identity'
435
+
436
+ export const getServerUser = createServerFn({ method: 'GET' }).handler(async () => {
437
+ const user = getUser()
438
+ return user ?? null
439
+ })
440
+ ```
441
+
442
+ ```tsx
443
+ // app/routes/login.tsx - browser-side auth for mutations
444
+ import { login, signup, onAuthChange } from '@netlify/identity'
445
+ import { getServerUser } from '~/server/auth'
446
+
447
+ export const Route = createFileRoute('/login')({
448
+ beforeLoad: async () => {
449
+ const user = await getServerUser()
450
+ if (user) throw redirect({ to: '/dashboard' })
451
+ },
452
+ component: Login,
453
+ })
454
+
455
+ function Login() {
456
+ const handleLogin = async (email: string, password: string) => {
457
+ await login(email, password) // browser-side: sets cookie + localStorage
458
+ window.location.href = '/dashboard'
459
+ }
460
+ // ...
461
+ }
462
+ ```
463
+
464
+ ```tsx
465
+ // app/routes/dashboard.tsx
466
+ import { logout } from '@netlify/identity'
467
+ import { getServerUser } from '~/server/auth'
468
+
469
+ export const Route = createFileRoute('/dashboard')({
470
+ beforeLoad: async () => {
471
+ const user = await getServerUser()
472
+ if (!user) throw redirect({ to: '/login' })
473
+ },
474
+ loader: async () => {
475
+ const user = await getServerUser()
476
+ return { user: user! }
477
+ },
478
+ component: Dashboard,
479
+ })
480
+
481
+ function Dashboard() {
482
+ const { user } = Route.useLoaderData()
483
+
484
+ const handleLogout = async () => {
485
+ await logout() // browser-side: clears cookie + localStorage
486
+ window.location.href = '/'
487
+ }
488
+ // ...
489
+ }
490
+ ```
491
+
492
+ 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.
493
+
494
+ ### Astro (SSR)
495
+
496
+ **Login via API endpoint (server-side pattern):**
497
+
498
+ ```ts
499
+ // src/pages/api/login.ts
500
+ import type { APIRoute } from 'astro'
501
+ import { login } from '@netlify/identity'
502
+
503
+ export const POST: APIRoute = async ({ request }) => {
504
+ const { email, password } = await request.json()
505
+
506
+ try {
507
+ await login(email, password)
508
+ return new Response(null, {
509
+ status: 302,
510
+ headers: { Location: '/dashboard' },
511
+ })
512
+ } catch (error) {
513
+ return Response.json({ error: (error as Error).message }, { status: 400 })
514
+ }
515
+ }
516
+ ```
517
+
518
+ ```astro
519
+ ---
520
+ // src/pages/dashboard.astro
521
+ import { getUser } from '@netlify/identity'
522
+
523
+ const user = getUser()
524
+ if (!user) return Astro.redirect('/login')
525
+ ---
526
+ <h1>Hello, {user.email}</h1>
527
+ ```
528
+
529
+ ### Handling OAuth callbacks in SPAs
530
+
531
+ All SPA frameworks need a callback handler that runs on page load to process OAuth redirects, email confirmations, and password recovery tokens.
532
+
533
+ ```tsx
534
+ // React component (works with Next.js, Remix, TanStack Start)
535
+ import { useEffect } from 'react'
536
+ import { handleAuthCallback } from '@netlify/identity'
537
+
538
+ export function CallbackHandler() {
539
+ useEffect(() => {
540
+ if (!window.location.hash) return
541
+
542
+ handleAuthCallback().then((result) => {
543
+ if (!result) return
544
+ if (result.type === 'invite') {
545
+ window.location.href = `/accept-invite?token=${result.token}`
546
+ } else {
547
+ window.location.href = '/dashboard'
548
+ }
549
+ })
550
+ }, [])
551
+
552
+ return null
553
+ }
554
+ ```
555
+
556
+ Mount this component in your root layout so it processes callbacks on any page.
557
+
278
558
  ## Guides
279
559
 
560
+ ### Listening for auth changes
561
+
562
+ 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`).
563
+
564
+ ```ts
565
+ import { onAuthChange } from '@netlify/identity'
566
+
567
+ const unsubscribe = onAuthChange((event, user) => {
568
+ switch (event) {
569
+ case 'login':
570
+ console.log('Logged in:', user?.email)
571
+ break
572
+ case 'logout':
573
+ console.log('Logged out')
574
+ break
575
+ case 'token_refresh':
576
+ console.log('Token refreshed for:', user?.email)
577
+ break
578
+ case 'user_updated':
579
+ console.log('User updated:', user?.email)
580
+ break
581
+ }
582
+ })
583
+
584
+ // Later, to stop listening:
585
+ unsubscribe()
586
+ ```
587
+
588
+ On the server, `onAuthChange` is a no-op and the returned unsubscribe function does nothing.
589
+
590
+ ### OAuth login
591
+
592
+ OAuth login is a two-step flow: redirect the user to the provider, then process the callback when they return.
593
+
594
+ **Step by step:**
595
+
596
+ ```ts
597
+ import { oauthLogin, handleAuthCallback } from '@netlify/identity'
598
+
599
+ // 1. Kick off the OAuth flow (e.g., from a "Sign in with GitHub" button).
600
+ // This navigates away from the page and does not return.
601
+ oauthLogin('github')
602
+ ```
603
+
604
+ ```ts
605
+ // 2. On page load, handle the redirect back from the provider.
606
+ const result = await handleAuthCallback()
607
+
608
+ if (result?.type === 'oauth') {
609
+ console.log('Logged in via OAuth:', result.user?.email)
610
+ }
611
+ ```
612
+
613
+ `handleAuthCallback()` exchanges the token in the URL hash, logs the user in, clears the hash, and emits a `'login'` event via `onAuthChange`.
614
+
280
615
  ### Password recovery
281
616
 
282
617
  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 +635,27 @@ if (result?.type === 'recovery') {
300
635
  }
301
636
  ```
302
637
 
638
+ ### Invite acceptance
639
+
640
+ 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.
641
+
642
+ **Step by step:**
643
+
644
+ ```ts
645
+ import { handleAuthCallback, acceptInvite } from '@netlify/identity'
646
+
647
+ // 1. On page load, handle the callback.
648
+ const result = await handleAuthCallback()
649
+
650
+ if (result?.type === 'invite' && result.token) {
651
+ // 2. The user is NOT logged in yet. Show a "set your password" form.
652
+ // When they submit:
653
+ const password = document.getElementById('password').value
654
+ const user = await acceptInvite(result.token, password)
655
+ console.log('Account created:', user.email)
656
+ }
657
+ ```
658
+
303
659
  ## License
304
660
 
305
661
  MIT