@netlify/agent-runner-cli 1.88.0-alpha.1 → 1.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin-local.js +54 -54
- package/dist/bin.js +53 -53
- package/dist/index.d.ts +2 -251
- package/dist/index.js +52 -52
- package/dist/skills/netlify-identity/SKILL.md +481 -198
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: netlify-identity
|
|
3
|
-
description: Add user authentication with Netlify Identity. Use when implementing signups, logins, password recovery, role-based access control, OAuth providers,
|
|
3
|
+
description: Add user authentication with Netlify Identity. Use when implementing signups, logins, password recovery, role-based access control, OAuth providers, protecting Netlify Functions, or server-side auth in SSR frameworks.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Netlify Identity
|
|
@@ -8,227 +8,404 @@ description: Add user authentication with Netlify Identity. Use when implementin
|
|
|
8
8
|
Netlify Identity is a user management service for signups, logins, password recovery, user metadata, and role-based
|
|
9
9
|
access control. It is built on [GoTrue](https://github.com/netlify/gotrue) and issues JSON Web Tokens (JWTs).
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**NEVER use `netlify-identity-widget` or `gotrue-js`. Always use `@netlify/identity`.** It provides a unified, headless
|
|
12
|
+
TypeScript API that works in both browser and server contexts (Netlify Functions, Edge Functions, SSR frameworks). It
|
|
13
|
+
replaces all previous Identity client libraries.
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @netlify/identity
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Identity is automatically enabled when the deploy includes Identity code. Default settings:
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
- **Registration** - Open (anyone can sign up). Change to Invite only in **Project configuration > Identity** if needed.
|
|
24
|
+
- **Autoconfirm** - Off (new signups require email confirmation). Enable in **Project configuration > Identity** to skip
|
|
25
|
+
confirmation during development.
|
|
26
|
+
|
|
27
|
+
For local development, use `netlify dev` so the Identity endpoint is available.
|
|
19
28
|
|
|
20
29
|
## Quick Start
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
Log in from the browser:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { login, getUser } from '@netlify/identity'
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
const user = await login('jane@example.com', 'password123')
|
|
37
|
+
console.log(`Hello, ${user.name}`)
|
|
38
|
+
|
|
39
|
+
// Later, check auth state
|
|
40
|
+
const currentUser = await getUser()
|
|
27
41
|
```
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
Protect a Netlify Function:
|
|
30
44
|
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
```typescript
|
|
46
|
+
// netlify/functions/protected.mts
|
|
47
|
+
import { getUser } from '@netlify/identity'
|
|
48
|
+
import type { Context } from '@netlify/functions'
|
|
49
|
+
|
|
50
|
+
export default async (req: Request, context: Context) => {
|
|
51
|
+
const user = await getUser()
|
|
52
|
+
if (!user) return new Response('Unauthorized', { status: 401 })
|
|
53
|
+
return Response.json({ id: user.id, email: user.email })
|
|
54
|
+
}
|
|
38
55
|
```
|
|
39
56
|
|
|
40
|
-
|
|
57
|
+
## Error Handling
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
simple site. It requires minimal code and automatically sets the `nf_jwt` cookie needed for role-based redirects.
|
|
44
|
-
- **Use gotrue-js** when the user needs custom login/signup forms, headless auth, or programmatic control over the auth
|
|
45
|
-
flow. Pass `setCookie: true` if you need role-based redirects (see below).
|
|
46
|
-
- **Never mix both** in the same page unless accessing `netlifyIdentity.gotrue` for low-level operations while using the
|
|
47
|
-
widget for UI.
|
|
59
|
+
`@netlify/identity` throws two error classes:
|
|
48
60
|
|
|
49
|
-
|
|
61
|
+
- **`AuthError`** - Thrown by auth operations (login, signup, logout, etc.). Has `message`, optional `status` (HTTP
|
|
62
|
+
status code from GoTrue), and optional `cause` (original error).
|
|
63
|
+
- **`MissingIdentityError`** - Thrown when Identity is not configured in the current environment (site doesn't have
|
|
64
|
+
Identity enabled, or not running via `netlify dev`).
|
|
50
65
|
|
|
51
|
-
|
|
66
|
+
`getUser()` and `isAuthenticated()` never throw - they return `null` and `false` respectively on failure.
|
|
52
67
|
|
|
53
|
-
|
|
54
|
-
- **Autoconfirm** — Off (new signups require email confirmation). Enable in **Project configuration > Identity** to skip confirmation during development.
|
|
68
|
+
### Try/Catch Pattern
|
|
55
69
|
|
|
56
|
-
|
|
70
|
+
```typescript
|
|
71
|
+
import { login, AuthError, MissingIdentityError } from '@netlify/identity'
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const user = await login(email, password)
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof MissingIdentityError) {
|
|
77
|
+
// Identity not configured - show setup instructions
|
|
78
|
+
showError('Identity is not enabled on this site.')
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (error instanceof AuthError) {
|
|
82
|
+
switch (error.status) {
|
|
83
|
+
case 401:
|
|
84
|
+
showError('Invalid email or password.')
|
|
85
|
+
break
|
|
86
|
+
case 403:
|
|
87
|
+
showError('Signups are not allowed for this site.')
|
|
88
|
+
break
|
|
89
|
+
case 422:
|
|
90
|
+
showError('Invalid input. Check your email and password.')
|
|
91
|
+
break
|
|
92
|
+
case 404:
|
|
93
|
+
showError('User not found.')
|
|
94
|
+
break
|
|
95
|
+
default:
|
|
96
|
+
showError(error.message)
|
|
97
|
+
}
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
throw error
|
|
101
|
+
}
|
|
102
|
+
```
|
|
57
103
|
|
|
58
|
-
###
|
|
104
|
+
### Common Status Codes
|
|
59
105
|
|
|
60
|
-
|
|
106
|
+
| Status | Meaning |
|
|
107
|
+
|--------|---------|
|
|
108
|
+
| 401 | Invalid credentials or expired token |
|
|
109
|
+
| 403 | Action not allowed (e.g., signups disabled) |
|
|
110
|
+
| 422 | Validation error (e.g., weak password, malformed email) |
|
|
111
|
+
| 404 | User or resource not found |
|
|
61
112
|
|
|
62
|
-
|
|
63
|
-
<script type="text/javascript" src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
|
|
113
|
+
## Authentication Flows
|
|
64
114
|
|
|
65
|
-
|
|
66
|
-
<div data-netlify-identity-menu></div>
|
|
115
|
+
### Login
|
|
67
116
|
|
|
68
|
-
|
|
69
|
-
|
|
117
|
+
```typescript
|
|
118
|
+
import { login, AuthError } from '@netlify/identity'
|
|
119
|
+
|
|
120
|
+
let loading = false
|
|
121
|
+
|
|
122
|
+
async function handleLogin(email: string, password: string) {
|
|
123
|
+
loading = true
|
|
124
|
+
try {
|
|
125
|
+
const user = await login(email, password)
|
|
126
|
+
showSuccess(`Welcome back, ${user.name ?? user.email}`)
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error instanceof AuthError) {
|
|
129
|
+
showError(error.status === 401 ? 'Invalid email or password.' : error.message)
|
|
130
|
+
}
|
|
131
|
+
} finally {
|
|
132
|
+
loading = false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
70
135
|
```
|
|
71
136
|
|
|
72
|
-
|
|
137
|
+
### Signup
|
|
73
138
|
|
|
74
|
-
|
|
139
|
+
After signup, check `user.emailVerified` to determine if the user was auto-confirmed (logged in immediately) or needs
|
|
140
|
+
to confirm their email first.
|
|
75
141
|
|
|
76
|
-
```
|
|
77
|
-
|
|
142
|
+
```typescript
|
|
143
|
+
import { signup, AuthError } from '@netlify/identity'
|
|
144
|
+
|
|
145
|
+
async function handleSignup(email: string, password: string, name: string) {
|
|
146
|
+
try {
|
|
147
|
+
const user = await signup(email, password, { full_name: name })
|
|
148
|
+
if (user.emailVerified) {
|
|
149
|
+
// Autoconfirm is ON - user is logged in
|
|
150
|
+
showSuccess('Account created. You are now logged in.')
|
|
151
|
+
} else {
|
|
152
|
+
// Autoconfirm is OFF - confirmation email sent, user is NOT logged in
|
|
153
|
+
showSuccess('Check your email to confirm your account.')
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error instanceof AuthError) {
|
|
157
|
+
showError(error.status === 403 ? 'Signups are not allowed.' : error.message)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
78
161
|
```
|
|
79
162
|
|
|
80
|
-
|
|
81
|
-
|
|
163
|
+
When autoconfirm is off, the confirmation email contains a link that redirects the user back to the site with
|
|
164
|
+
`#confirmation_token=<token>` in the URL hash. `handleAuthCallback()` processes this automatically - it calls
|
|
165
|
+
`confirmEmail()` under the hood, logs the user in, and returns `{ type: 'confirmation', user }`. This is why
|
|
166
|
+
`handleAuthCallback()` must be called on page load (see the OAuth section below for the full switch).
|
|
82
167
|
|
|
83
|
-
|
|
84
|
-
container: '#netlify-modal', // defaults to document.body
|
|
85
|
-
locale: 'en', // language code (en, fr, es, pt, de, etc.)
|
|
86
|
-
// APIUrl: 'https://your-site.netlify.app/.netlify/identity', // only for non-Netlify hosts
|
|
87
|
-
})
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Set `APIUrl` only when the app is served from a different domain than the Identity endpoint (Cordova, Electron, or
|
|
91
|
-
custom domains).
|
|
168
|
+
### OAuth
|
|
92
169
|
|
|
93
|
-
|
|
170
|
+
OAuth is a two-step flow: `oauthLogin(provider)` redirects away from the site, then `handleAuthCallback()` processes
|
|
171
|
+
the redirect when the user returns.
|
|
94
172
|
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
netlifyIdentity.on('login', (user) => console.log('login', user))
|
|
98
|
-
netlifyIdentity.on('logout', () => console.log('Logged out'))
|
|
99
|
-
netlifyIdentity.on('error', (err) => console.error('Error', err))
|
|
173
|
+
```typescript
|
|
174
|
+
import { oauthLogin } from '@netlify/identity'
|
|
100
175
|
|
|
101
|
-
|
|
102
|
-
|
|
176
|
+
// Step 1: Redirect to OAuth provider (this navigates away - never returns)
|
|
177
|
+
function handleOAuthClick(provider: 'google' | 'github' | 'gitlab' | 'bitbucket') {
|
|
178
|
+
oauthLogin(provider)
|
|
179
|
+
}
|
|
103
180
|
```
|
|
104
181
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
182
|
+
```typescript
|
|
183
|
+
import { handleAuthCallback, AuthError } from '@netlify/identity'
|
|
184
|
+
|
|
185
|
+
// Step 2: Process the redirect on page load
|
|
186
|
+
async function processCallback() {
|
|
187
|
+
try {
|
|
188
|
+
const result = await handleAuthCallback()
|
|
189
|
+
if (!result) return // No callback hash present - normal page load
|
|
190
|
+
|
|
191
|
+
switch (result.type) {
|
|
192
|
+
case 'oauth':
|
|
193
|
+
showSuccess(`Logged in as ${result.user?.email}`)
|
|
194
|
+
break
|
|
195
|
+
case 'confirmation':
|
|
196
|
+
showSuccess('Email confirmed. You are now logged in.')
|
|
197
|
+
break
|
|
198
|
+
case 'recovery':
|
|
199
|
+
// User is authenticated but must set a new password
|
|
200
|
+
showPasswordResetForm(result.user)
|
|
201
|
+
break
|
|
202
|
+
case 'invite':
|
|
203
|
+
// User must set a password to accept the invite
|
|
204
|
+
showInviteAcceptForm(result.token)
|
|
205
|
+
break
|
|
206
|
+
case 'email_change':
|
|
207
|
+
showSuccess('Email address updated.')
|
|
208
|
+
break
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error instanceof AuthError) {
|
|
212
|
+
showError(error.message)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
117
216
|
```
|
|
118
217
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
### Install
|
|
218
|
+
Always call `handleAuthCallback()` on page load in any app that uses OAuth, password recovery, invites, or email
|
|
219
|
+
confirmation. It handles all callback types via the URL hash.
|
|
122
220
|
|
|
123
|
-
|
|
124
|
-
npm install gotrue-js
|
|
125
|
-
```
|
|
221
|
+
### Password Recovery
|
|
126
222
|
|
|
127
|
-
|
|
223
|
+
Three-step flow: request recovery email, handle the callback, then set a new password.
|
|
128
224
|
|
|
129
225
|
```typescript
|
|
130
|
-
import
|
|
226
|
+
import { requestPasswordRecovery, handleAuthCallback, updateUser, AuthError } from '@netlify/identity'
|
|
227
|
+
|
|
228
|
+
// Step 1: Send recovery email
|
|
229
|
+
async function handleForgotPassword(email: string) {
|
|
230
|
+
try {
|
|
231
|
+
await requestPasswordRecovery(email)
|
|
232
|
+
showSuccess('Check your email for a password reset link.')
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (error instanceof AuthError) showError(error.message)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
131
237
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
238
|
+
// Step 2: handleAuthCallback() returns { type: 'recovery', user } - show password reset form
|
|
239
|
+
// (See the handleAuthCallback switch above)
|
|
240
|
+
|
|
241
|
+
// Step 3: Set new password
|
|
242
|
+
async function handlePasswordReset(newPassword: string) {
|
|
243
|
+
try {
|
|
244
|
+
await updateUser({ password: newPassword })
|
|
245
|
+
showSuccess('Password updated.')
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error instanceof AuthError) showError(error.message)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
136
250
|
```
|
|
137
251
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
when you manage tokens entirely in JavaScript and don't need CDN-level access control.
|
|
252
|
+
Note: The recovery callback fires a `'recovery'` auth event, not `'login'`. The user is authenticated but should be
|
|
253
|
+
prompted to set a new password before navigating away.
|
|
141
254
|
|
|
142
|
-
###
|
|
255
|
+
### Invite Acceptance
|
|
143
256
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const response = await auth.signup(email, password)
|
|
147
|
-
// Pass user_metadata during signup: auth.signup(email, password, { full_name: 'Jane' })
|
|
257
|
+
When a user clicks an invite link, `handleAuthCallback()` returns `{ type: 'invite', user: null, token }`. Use the
|
|
258
|
+
token to accept the invite and set a password.
|
|
148
259
|
|
|
149
|
-
|
|
150
|
-
|
|
260
|
+
```typescript
|
|
261
|
+
import { acceptInvite, AuthError } from '@netlify/identity'
|
|
262
|
+
|
|
263
|
+
async function handleAcceptInvite(token: string, password: string) {
|
|
264
|
+
try {
|
|
265
|
+
const user = await acceptInvite(token, password)
|
|
266
|
+
showSuccess(`Welcome, ${user.email}! Your account is ready.`)
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error instanceof AuthError) showError(error.message)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
151
272
|
|
|
152
|
-
|
|
153
|
-
const user = await auth.login(email, password, true) // remember = persist session
|
|
273
|
+
### Email Change
|
|
154
274
|
|
|
155
|
-
|
|
156
|
-
|
|
275
|
+
When a user verifies an email change, `handleAuthCallback()` returns `{ type: 'email_change', user }`. This requires an
|
|
276
|
+
active browser session - the user must be logged in when clicking the verification link.
|
|
157
277
|
|
|
158
|
-
|
|
159
|
-
|
|
278
|
+
```typescript
|
|
279
|
+
import { verifyEmailChange, AuthError } from '@netlify/identity'
|
|
280
|
+
|
|
281
|
+
// If you need to verify programmatically with a token:
|
|
282
|
+
async function handleEmailChangeVerification(token: string) {
|
|
283
|
+
try {
|
|
284
|
+
const user = await verifyEmailChange(token)
|
|
285
|
+
showSuccess(`Email updated to ${user.email}`)
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (error instanceof AuthError) showError(error.message)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
160
291
|
|
|
161
|
-
|
|
162
|
-
await user.update({ email: 'new@example.com', password: 'newpassword' })
|
|
163
|
-
await user.update({ data: { full_name: 'Jane Doe' } }) // update user_metadata
|
|
292
|
+
### Session Hydration
|
|
164
293
|
|
|
165
|
-
|
|
166
|
-
|
|
294
|
+
`hydrateSession()` bridges server-set cookies to the browser session. Call it on page load when using server-side login
|
|
295
|
+
(e.g., login inside a Netlify Function followed by a redirect).
|
|
167
296
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const user = await auth.recover(recoveryToken, true) // remember = persist session
|
|
297
|
+
```typescript
|
|
298
|
+
import { hydrateSession } from '@netlify/identity'
|
|
171
299
|
|
|
172
|
-
//
|
|
173
|
-
const
|
|
300
|
+
// On page load
|
|
301
|
+
const user = await hydrateSession()
|
|
302
|
+
if (user) {
|
|
303
|
+
// Browser session is now in sync with server-set cookies
|
|
304
|
+
}
|
|
174
305
|
```
|
|
175
306
|
|
|
176
|
-
|
|
307
|
+
Note: `getUser()` auto-hydrates from the `nf_jwt` cookie if no browser session exists, so explicit `hydrateSession()`
|
|
308
|
+
is only needed when you want to restore the full session (including token refresh timers) after a server-side login.
|
|
177
309
|
|
|
178
|
-
|
|
179
|
-
`error.data`, or `error.status` respectively.
|
|
310
|
+
## Auth Events
|
|
180
311
|
|
|
181
|
-
|
|
312
|
+
Subscribe to auth state changes with `onAuthChange`. Returns an unsubscribe function. No-op on server.
|
|
182
313
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
314
|
+
```typescript
|
|
315
|
+
import { onAuthChange, AUTH_EVENTS } from '@netlify/identity'
|
|
316
|
+
|
|
317
|
+
const unsubscribe = onAuthChange((event, user) => {
|
|
318
|
+
switch (event) {
|
|
319
|
+
case AUTH_EVENTS.LOGIN:
|
|
320
|
+
console.log('User logged in:', user?.email)
|
|
321
|
+
break
|
|
322
|
+
case AUTH_EVENTS.LOGOUT:
|
|
323
|
+
console.log('User logged out')
|
|
324
|
+
break
|
|
325
|
+
case AUTH_EVENTS.TOKEN_REFRESH:
|
|
326
|
+
// Token auto-refreshed in background
|
|
327
|
+
break
|
|
328
|
+
case AUTH_EVENTS.USER_UPDATED:
|
|
329
|
+
console.log('User profile updated:', user?.email)
|
|
330
|
+
break
|
|
331
|
+
case AUTH_EVENTS.RECOVERY:
|
|
332
|
+
// Recovery token processed - prompt for new password
|
|
333
|
+
console.log('Password recovery initiated')
|
|
334
|
+
break
|
|
335
|
+
}
|
|
336
|
+
})
|
|
187
337
|
|
|
188
|
-
|
|
338
|
+
// Later: unsubscribe()
|
|
339
|
+
```
|
|
189
340
|
|
|
190
|
-
|
|
341
|
+
Auth events are automatically detected across browser tabs via the storage event listener - no extra setup needed.
|
|
191
342
|
|
|
192
|
-
|
|
193
|
-
(Identity event functions, protected functions with admin token). Do not allow users to set their own roles.
|
|
194
|
-
- **`user_metadata`** — User-controlled. Users can update this via `user.update({ data: { ... } })`.
|
|
343
|
+
## Settings-Driven UI
|
|
195
344
|
|
|
196
|
-
|
|
345
|
+
Fetch the project's Identity settings to conditionally render signup forms and OAuth buttons.
|
|
197
346
|
|
|
198
|
-
|
|
347
|
+
```typescript
|
|
348
|
+
import { getSettings } from '@netlify/identity'
|
|
199
349
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
status = 200
|
|
205
|
-
conditions = { Role = ["admin"] }
|
|
350
|
+
const settings = await getSettings()
|
|
351
|
+
// settings.autoconfirm - boolean, whether email confirmation is skipped
|
|
352
|
+
// settings.disableSignup - boolean, whether registration is closed
|
|
353
|
+
// settings.providers - Record<AuthProvider, boolean>, e.g. { google: true, github: true, ... }
|
|
206
354
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
355
|
+
// Conditionally render signup
|
|
356
|
+
if (!settings.disableSignup) {
|
|
357
|
+
showSignupForm()
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Conditionally render OAuth buttons
|
|
361
|
+
for (const [provider, enabled] of Object.entries(settings.providers)) {
|
|
362
|
+
if (enabled) {
|
|
363
|
+
showOAuthButton(provider)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
211
366
|
```
|
|
212
367
|
|
|
213
|
-
|
|
214
|
-
|
|
368
|
+
## Full API Reference
|
|
369
|
+
|
|
370
|
+
For the complete API reference - all function signatures, type definitions, OAuth helpers, admin operations, session
|
|
371
|
+
management, auth events, and framework-specific integration examples - read the package README:
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
node_modules/@netlify/identity/README.md
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
The README is shipped with the npm package and is always in sync with the installed version.
|
|
378
|
+
|
|
379
|
+
## SSR Integration Patterns
|
|
215
380
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
381
|
+
For SSR frameworks, the recommended pattern is:
|
|
382
|
+
|
|
383
|
+
- **Browser-side** for auth mutations: `login()`, `signup()`, `logout()`, `oauthLogin()`
|
|
384
|
+
- **Server-side** for reading auth state: `getUser()`, `getSettings()`, `getIdentityConfig()`
|
|
385
|
+
|
|
386
|
+
Browser-side auth mutations set the `nf_jwt` cookie and localStorage, and emit `onAuthChange` events. The
|
|
387
|
+
server reads the cookie on the next request.
|
|
388
|
+
|
|
389
|
+
The library also supports server-side mutations (`login()`, `signup()`, `logout()` inside Netlify Functions), but these
|
|
390
|
+
require the Netlify Functions runtime to set cookies. After a server-side mutation, use a full page navigation so the
|
|
391
|
+
browser sends the new cookie.
|
|
392
|
+
|
|
393
|
+
Always use `window.location.href` (not framework router navigation) after server-side auth mutations in Next.js,
|
|
394
|
+
TanStack Start, and SvelteKit. Remix `redirect()` is safe because Remix actions return real HTTP responses.
|
|
219
395
|
|
|
220
396
|
## Identity Event Functions
|
|
221
397
|
|
|
222
398
|
Special serverless functions that trigger on Identity lifecycle events. These use the **legacy named `handler` export**
|
|
223
399
|
(not the modern default export) because they receive `event.body` containing the user payload.
|
|
224
400
|
|
|
225
|
-
|
|
401
|
+
Always use the legacy named `handler` export (not default export) for Identity event functions. The filename must match
|
|
402
|
+
the event name exactly (e.g., `netlify/functions/identity-signup.mts`).
|
|
226
403
|
|
|
227
|
-
- `identity-signup`
|
|
228
|
-
- `identity-login` — fires on each login
|
|
229
|
-
- `identity-validate` — fires during signup before the user is created; return a non-200 status to reject the signup
|
|
404
|
+
**Event names:** `identity-validate`, `identity-signup`, `identity-login`
|
|
230
405
|
|
|
231
|
-
|
|
406
|
+
- `identity-signup` - fires when a new user signs up (email/password or OAuth)
|
|
407
|
+
- `identity-login` - fires on each login
|
|
408
|
+
- `identity-validate` - fires during signup before the user is created; return a non-200 status to reject
|
|
232
409
|
|
|
233
410
|
### Example: Assign Default Role on Signup
|
|
234
411
|
|
|
@@ -253,77 +430,183 @@ const handler: Handler = async (event: HandlerEvent, context: HandlerContext) =>
|
|
|
253
430
|
export { handler }
|
|
254
431
|
```
|
|
255
432
|
|
|
256
|
-
The response body replaces `app_metadata` and/or `user_metadata` on the user record
|
|
433
|
+
The response body replaces `app_metadata` and/or `user_metadata` on the user record - include all fields you want to
|
|
257
434
|
keep, not just new ones.
|
|
258
435
|
|
|
259
|
-
|
|
436
|
+
For bulk user management or role changes outside lifecycle events, use the `admin` API instead of Identity event
|
|
437
|
+
functions.
|
|
438
|
+
|
|
439
|
+
## Roles and Authorization
|
|
440
|
+
|
|
441
|
+
### Metadata Types
|
|
442
|
+
|
|
443
|
+
- **`app_metadata.roles`** - Server-controlled. Only settable via the Netlify UI, admin API, or Identity event functions.
|
|
444
|
+
Do not allow users to set their own roles.
|
|
445
|
+
- **`user_metadata`** - User-controlled. Users can update this via `updateUser({ data: { ... } })`.
|
|
260
446
|
|
|
261
|
-
|
|
447
|
+
### Role-Based Redirects
|
|
448
|
+
|
|
449
|
+
Use `netlify.toml` to restrict paths by role:
|
|
450
|
+
|
|
451
|
+
```toml
|
|
452
|
+
[[redirects]]
|
|
453
|
+
from = "/admin/*"
|
|
454
|
+
to = "/admin/:splat"
|
|
455
|
+
status = 200
|
|
456
|
+
conditions = { Role = ["admin"] }
|
|
457
|
+
|
|
458
|
+
[[redirects]]
|
|
459
|
+
from = "/admin/*"
|
|
460
|
+
to = "/"
|
|
461
|
+
status = 302
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Rules are evaluated top-to-bottom. The first redirect matches users with the `admin` role; everyone else falls through
|
|
465
|
+
to the second rule and is redirected away.
|
|
466
|
+
|
|
467
|
+
**How it works:** The `nf_jwt` cookie is read by the CDN to evaluate `conditions = { Role = [...] }`. Without this
|
|
468
|
+
cookie, role-based redirects will not work.
|
|
469
|
+
|
|
470
|
+
## Common Errors & Solutions
|
|
471
|
+
|
|
472
|
+
### "Signups not allowed for this instance" (403)
|
|
473
|
+
|
|
474
|
+
**Cause:** Registration is set to Invite only.
|
|
475
|
+
|
|
476
|
+
**Fix:**
|
|
477
|
+
|
|
478
|
+
1. Change to Open in **Project configuration > Identity**
|
|
479
|
+
2. Or invite users from the Identity tab in the Netlify UI
|
|
262
480
|
|
|
263
481
|
```typescript
|
|
264
|
-
|
|
265
|
-
|
|
482
|
+
if (error instanceof AuthError && error.status === 403) {
|
|
483
|
+
showError('Signups are disabled. Contact the site admin for an invite.')
|
|
484
|
+
}
|
|
485
|
+
```
|
|
266
486
|
|
|
267
|
-
|
|
268
|
-
const { identity, user } = context.clientContext || {}
|
|
487
|
+
### Invalid credentials (401)
|
|
269
488
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
489
|
+
**Cause:** Wrong email or password on login.
|
|
490
|
+
|
|
491
|
+
**Fix:** Show a user-facing error and let the user retry. Do not reveal whether the email or password was wrong.
|
|
273
492
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
// user.user_metadata — user-controlled data
|
|
278
|
-
|
|
279
|
-
// Use identity.token (admin token) to call the Identity admin API:
|
|
280
|
-
const adminAuthHeader = `Bearer ${identity.token}`
|
|
281
|
-
const response = await fetch(`${identity.url}/admin/users/${user.sub}`, {
|
|
282
|
-
method: 'PUT',
|
|
283
|
-
headers: { Authorization: adminAuthHeader },
|
|
284
|
-
body: JSON.stringify({ app_metadata: { roles: ['editor'] } }),
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
const data = await response.json()
|
|
288
|
-
return { statusCode: 200, body: JSON.stringify(data) }
|
|
493
|
+
```typescript
|
|
494
|
+
if (error instanceof AuthError && error.status === 401) {
|
|
495
|
+
showError('Invalid email or password.')
|
|
289
496
|
}
|
|
497
|
+
```
|
|
290
498
|
|
|
291
|
-
|
|
499
|
+
### "Email not confirmed"
|
|
500
|
+
|
|
501
|
+
**Cause:** User tries to log in before confirming their email (autoconfirm is off).
|
|
502
|
+
|
|
503
|
+
**Fix:** Tell the user to check their inbox. Optionally provide a way to resend the confirmation email via `signup()`
|
|
504
|
+
with the same credentials.
|
|
505
|
+
|
|
506
|
+
### "Token expired" / 401 on API calls
|
|
507
|
+
|
|
508
|
+
**Cause:** Stale access token.
|
|
509
|
+
|
|
510
|
+
**Fix:**
|
|
511
|
+
|
|
512
|
+
1. Always use `getUser()` before authenticated requests - it auto-hydrates from cookies
|
|
513
|
+
2. In the browser, the library auto-refreshes tokens via `startTokenRefresh()`
|
|
514
|
+
3. On the server, call `refreshSession()` in middleware to handle near-expiry tokens
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
const newToken = await refreshSession()
|
|
518
|
+
if (newToken) {
|
|
519
|
+
// Token was refreshed - retry the request
|
|
520
|
+
}
|
|
292
521
|
```
|
|
293
522
|
|
|
294
|
-
|
|
295
|
-
|
|
523
|
+
### Identity event function not triggering
|
|
524
|
+
|
|
525
|
+
**Cause:** Filename or export format does not match expected convention.
|
|
296
526
|
|
|
297
|
-
|
|
527
|
+
**Fix:**
|
|
298
528
|
|
|
299
|
-
|
|
529
|
+
1. Verify filename matches exactly: `identity-signup`, `identity-validate`, or `identity-login`
|
|
530
|
+
2. Place in `netlify/functions/` with `.mts` or `.mjs` extension
|
|
531
|
+
3. Use named `handler` export (not default export)
|
|
300
532
|
|
|
301
|
-
|
|
533
|
+
### `MissingIdentityError`
|
|
302
534
|
|
|
303
|
-
|
|
535
|
+
**Cause:** Identity is not configured in the current environment.
|
|
304
536
|
|
|
305
|
-
|
|
537
|
+
**Fix:**
|
|
306
538
|
|
|
307
|
-
|
|
539
|
+
1. Ensure Identity is enabled on the project
|
|
540
|
+
2. Use `netlify dev` for local development so the Identity endpoint is available
|
|
308
541
|
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
542
|
+
```typescript
|
|
543
|
+
if (error instanceof MissingIdentityError) {
|
|
544
|
+
showError('Identity is not enabled. Run "netlify dev" or enable Identity in project settings.')
|
|
545
|
+
}
|
|
313
546
|
```
|
|
314
547
|
|
|
315
|
-
|
|
548
|
+
### `AuthError` on server - missing Netlify runtime
|
|
316
549
|
|
|
317
|
-
|
|
550
|
+
**Cause:** Server-side `login()`, `signup()`, or `logout()` require the Netlify Functions runtime to set cookies.
|
|
551
|
+
|
|
552
|
+
**Fix:**
|
|
553
|
+
|
|
554
|
+
1. Deploy to Netlify to use server-side auth mutations
|
|
555
|
+
2. Or use `netlify dev` for local development
|
|
556
|
+
|
|
557
|
+
### "User not found" after OAuth login
|
|
558
|
+
|
|
559
|
+
**Cause:** OAuth provider is not enabled for the project.
|
|
560
|
+
|
|
561
|
+
**Fix:**
|
|
562
|
+
|
|
563
|
+
1. Enable the provider in **Project configuration > Identity > External providers**
|
|
564
|
+
2. Users are created automatically on first OAuth login
|
|
565
|
+
|
|
566
|
+
### Account operations fail after server-side login
|
|
567
|
+
|
|
568
|
+
**Cause:** Browser-side session is not bootstrapped from server-set cookies.
|
|
569
|
+
|
|
570
|
+
**Fix:** Call `hydrateSession()` on page load to bridge server-set cookies to the browser session. Then use
|
|
571
|
+
`updateUser()`, `verifyEmailChange()`, or other account operations.
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
import { hydrateSession, updateUser } from '@netlify/identity'
|
|
575
|
+
|
|
576
|
+
// On page load after server-side login
|
|
577
|
+
await hydrateSession()
|
|
578
|
+
|
|
579
|
+
// Now account operations work
|
|
580
|
+
await updateUser({ data: { full_name: 'Jane' } })
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### "Email change verification requires an active browser session"
|
|
584
|
+
|
|
585
|
+
**Cause:** `verifyEmailChange()` was called without an active session. The user must be logged in when clicking the
|
|
586
|
+
email change verification link.
|
|
587
|
+
|
|
588
|
+
**Fix:** Ensure the user is logged in before processing the `email_change` callback. If the session expired, prompt
|
|
589
|
+
the user to log in first.
|
|
590
|
+
|
|
591
|
+
### "No user is currently logged in"
|
|
592
|
+
|
|
593
|
+
**Cause:** An account operation (`updateUser`, `verifyEmailChange`) was called without an authenticated user.
|
|
594
|
+
|
|
595
|
+
**Fix:** Check `getUser()` before calling account operations. If `null`, redirect to login.
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
const user = await getUser()
|
|
599
|
+
if (!user) {
|
|
600
|
+
redirectToLogin()
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
await updateUser({ data: { full_name: 'Jane' } })
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Stale session - user deleted server-side
|
|
607
|
+
|
|
608
|
+
**Cause:** `getUser()` returns `null` when the `nf_jwt` cookie is gone but localStorage still has a stale
|
|
609
|
+
session. This happens when a user is deleted via the admin API or Netlify UI.
|
|
318
610
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
- **"Token expired" / 401 on API calls** — Stale access token. Always call `user.jwt()` or `netlifyIdentity.refresh()`
|
|
322
|
-
before authenticated requests.
|
|
323
|
-
- **Identity event function not triggering** — Verify filename matches exactly (`identity-signup`, `identity-validate`,
|
|
324
|
-
or `identity-login`), is in `netlify/functions/`, and uses `.mts`/`.mjs`.
|
|
325
|
-
- **`clientContext` is undefined** — Use named `handler` export (not `export default`), send `Authorization: Bearer`
|
|
326
|
-
header, and verify token is fresh.
|
|
327
|
-
- **Admin methods not working locally** — `identity.token` is unavailable in local dev. Deploy to Netlify to test.
|
|
328
|
-
- **"User not found" after OAuth login** — Enable the OAuth provider in Project configuration > Identity > External
|
|
329
|
-
providers. Users are created on first OAuth login.
|
|
611
|
+
**Fix:** `getUser()` handles this gracefully - it returns `null` and the stale localStorage entry is ignored. Always
|
|
612
|
+
check for `null` before using the user object.
|