@netlify/identity 0.3.0-alpha.7 → 0.3.1-alpha.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 +44 -18
- package/dist/index.cjs +93 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -2
- package/dist/index.d.ts +33 -2
- package/dist/index.js +92 -0
- package/dist/index.js.map +1 -1
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @netlify/identity
|
|
2
2
|
|
|
3
|
-
A lightweight, no-config headless authentication library for projects using Netlify Identity
|
|
3
|
+
A lightweight, no-config headless authentication library for projects using Netlify Identity. Works in both browser and server contexts.
|
|
4
4
|
|
|
5
5
|
> **Status:** Beta. The API may change before 1.0.
|
|
6
6
|
|
|
@@ -17,14 +17,14 @@ A lightweight, no-config headless authentication library for projects using Netl
|
|
|
17
17
|
| [`netlify-identity-widget`](https://github.com/netlify/netlify-identity-widget) | Pre-built login/signup modal (HTML + CSS) | You want a drop-in UI component with no custom design |
|
|
18
18
|
| [`gotrue-js`](https://github.com/netlify/gotrue-js) | Low-level GoTrue HTTP client (browser only) | You're building your own auth wrapper and need direct API access |
|
|
19
19
|
|
|
20
|
-
This library
|
|
20
|
+
This library provides a unified API that works in both browser and server contexts, handles cookie management, and normalizes the user object. You do not need to install `gotrue-js` or the widget separately.
|
|
21
21
|
|
|
22
22
|
## Table of contents
|
|
23
23
|
|
|
24
24
|
- [Installation](#installation)
|
|
25
25
|
- [Quick start](#quick-start)
|
|
26
26
|
- [API](#api)
|
|
27
|
-
- [Functions](#functions) -- `getUser`, `login`, `signup`, `logout`, `oauthLogin`, `handleAuthCallback`, `onAuthChange`, `hydrateSession`, and more
|
|
27
|
+
- [Functions](#functions) -- `getUser`, `login`, `signup`, `logout`, `oauthLogin`, `handleAuthCallback`, `onAuthChange`, `hydrateSession`, `refreshSession`, and more
|
|
28
28
|
- [Admin Operations](#admin-operations) -- `admin.listUsers`, `admin.getUser`, `admin.createUser`, `admin.updateUser`, `admin.deleteUser`
|
|
29
29
|
- [Types](#types) -- `User`, `AuthEvent`, `CallbackResult`, `Settings`, `Admin`, `ListUsersOptions`, `CreateUserParams`, etc.
|
|
30
30
|
- [Errors](#errors) -- `AuthError`, `MissingIdentityError`
|
|
@@ -132,7 +132,7 @@ login(email: string, password: string): Promise<User>
|
|
|
132
132
|
|
|
133
133
|
Logs in with email and password. Works in both browser and server contexts.
|
|
134
134
|
|
|
135
|
-
In the browser,
|
|
135
|
+
In the browser, emits a `'login'` event. On the server (Netlify Functions, Edge Functions), calls the Identity API directly and sets the `nf_jwt` cookie via the Netlify runtime.
|
|
136
136
|
|
|
137
137
|
**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.
|
|
138
138
|
|
|
@@ -158,7 +158,7 @@ logout(): Promise<void>
|
|
|
158
158
|
|
|
159
159
|
Logs out the current user and clears the session. Works in both browser and server contexts.
|
|
160
160
|
|
|
161
|
-
In the browser,
|
|
161
|
+
In the browser, emits a `'logout'` event. On the server, calls the Identity `/logout` endpoint with the JWT from the `nf_jwt` cookie, then deletes the cookie. Auth cookies are always cleared, even if the server call fails.
|
|
162
162
|
|
|
163
163
|
**Throws:** In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
|
|
164
164
|
|
|
@@ -198,11 +198,11 @@ Subscribes to auth state changes (login, logout, token refresh, user updates, an
|
|
|
198
198
|
hydrateSession(): Promise<User | null>
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
-
Bootstraps the browser-side
|
|
201
|
+
Bootstraps the browser-side 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.
|
|
202
202
|
|
|
203
|
-
**When to use:** After a server-side login (e.g., via a Netlify Function or Server Action), the `nf_jwt` cookie is set but
|
|
203
|
+
**When to use:** After a server-side login (e.g., via a Netlify Function or Server Action), the `nf_jwt` cookie is set but no browser session exists yet. `getUser()` calls `hydrateSession()` automatically, but account operations like `updateUser()` or `verifyEmailChange()` require a live browser session. Call `hydrateSession()` explicitly if you need the session ready before calling those operations.
|
|
204
204
|
|
|
205
|
-
If a
|
|
205
|
+
If a browser session already exists (e.g., from a browser-side login), this is a no-op and returns the existing user.
|
|
206
206
|
|
|
207
207
|
```ts
|
|
208
208
|
import { hydrateSession, updateUser } from '@netlify/identity'
|
|
@@ -214,6 +214,30 @@ await hydrateSession()
|
|
|
214
214
|
await updateUser({ data: { full_name: 'Jane Doe' } })
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
+
#### `refreshSession`
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
refreshSession(): Promise<string | null>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Refreshes an expired or near-expired session. Returns the new access token on success, or `null` if no refresh is needed or the refresh token is invalid/missing.
|
|
224
|
+
|
|
225
|
+
**Browser:** Checks if the current access token is near expiry and refreshes it if needed, syncing the new token to the `nf_jwt` cookie. Note: in the browser, the library automatically refreshes tokens in the background after `login()`, `hydrateSession()`, or `handleAuthCallback()`, so you typically don't need to call this manually.
|
|
226
|
+
|
|
227
|
+
**Server:** Reads the `nf_jwt` and `nf_refresh` cookies. If the access token is expired or within 60 seconds of expiry, exchanges the refresh token for a new access token via the Identity `/token` endpoint and updates both cookies on the response. Call this in framework middleware or at the start of server-side request handlers to ensure the JWT is valid for downstream processing.
|
|
228
|
+
|
|
229
|
+
**Throws:** `AuthError` on network failure or if the Identity endpoint URL cannot be determined. Does **not** throw for invalid/expired refresh tokens (returns `null` instead).
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
// Example: Astro middleware
|
|
233
|
+
import { refreshSession } from '@netlify/identity'
|
|
234
|
+
|
|
235
|
+
export async function onRequest(context, next) {
|
|
236
|
+
await refreshSession()
|
|
237
|
+
return next()
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
217
241
|
#### `requestPasswordRecovery`
|
|
218
242
|
|
|
219
243
|
```ts
|
|
@@ -279,7 +303,7 @@ Updates the current user's metadata or credentials. Requires an active session.
|
|
|
279
303
|
The `admin` namespace provides user management functions for administrators. These work in two contexts:
|
|
280
304
|
|
|
281
305
|
- **Server:** Uses the operator token from the Netlify runtime for full admin access. No logged-in user required.
|
|
282
|
-
- **Browser:** Uses the logged-in user's JWT
|
|
306
|
+
- **Browser:** Uses the logged-in user's JWT. The user must have an admin role.
|
|
283
307
|
|
|
284
308
|
```ts
|
|
285
309
|
import { admin } from '@netlify/identity'
|
|
@@ -315,7 +339,7 @@ export default async (req: Request, context: Context) => {
|
|
|
315
339
|
admin.listUsers(options?: ListUsersOptions): Promise<User[]>
|
|
316
340
|
```
|
|
317
341
|
|
|
318
|
-
Lists all users. Pagination options are supported on the server; they are ignored in the browser
|
|
342
|
+
Lists all users. Pagination options are supported on the server; they are ignored in the browser.
|
|
319
343
|
|
|
320
344
|
**Throws:** `AuthError` if the operator token is missing (server) or no user is logged in (browser).
|
|
321
345
|
|
|
@@ -460,7 +484,7 @@ interface ListUsersOptions {
|
|
|
460
484
|
}
|
|
461
485
|
```
|
|
462
486
|
|
|
463
|
-
Pagination options for `admin.listUsers()`. Only used on the server; pagination is ignored in the browser
|
|
487
|
+
Pagination options for `admin.listUsers()`. Only used on the server; pagination is ignored in the browser.
|
|
464
488
|
|
|
465
489
|
#### `CreateUserParams`
|
|
466
490
|
|
|
@@ -472,7 +496,7 @@ interface CreateUserParams {
|
|
|
472
496
|
}
|
|
473
497
|
```
|
|
474
498
|
|
|
475
|
-
Parameters for `admin.createUser()`. Optional `data` is spread into the
|
|
499
|
+
Parameters for `admin.createUser()`. Optional `data` is spread into the request body as top-level attributes (use it to set `app_metadata`, `user_metadata`, `role`, etc.).
|
|
476
500
|
|
|
477
501
|
#### `Admin`
|
|
478
502
|
|
|
@@ -506,7 +530,7 @@ Constants for auth event names. Use these instead of string literals for type sa
|
|
|
506
530
|
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
507
531
|
| `LOGIN` | `login()`, `signup()` (with autoconfirm), `recoverPassword()`, `handleAuthCallback()` (OAuth/confirmation), `hydrateSession()` |
|
|
508
532
|
| `LOGOUT` | `logout()` |
|
|
509
|
-
| `TOKEN_REFRESH` |
|
|
533
|
+
| `TOKEN_REFRESH` | The library's auto-refresh timer refreshes an expiring access token and syncs the new token to the `nf_jwt` cookie. Fires automatically after `login()`, `hydrateSession()`, or `handleAuthCallback()` establishes a session. |
|
|
510
534
|
| `USER_UPDATED` | `updateUser()`, `verifyEmailChange()`, `handleAuthCallback()` (email change) |
|
|
511
535
|
| `RECOVERY` | `handleAuthCallback()` (recovery token only). The user is authenticated but has **not** set a new password yet. Listen for this to redirect to a password reset form. `recoverPassword()` emits `LOGIN` instead because it completes both steps (token redemption + password change). |
|
|
512
536
|
|
|
@@ -564,7 +588,7 @@ For SSR frameworks (Next.js, Remix, Astro, TanStack Start), the recommended patt
|
|
|
564
588
|
- **Browser-side** for auth mutations: `login()`, `signup()`, `logout()`, `oauthLogin()`
|
|
565
589
|
- **Server-side** for reading auth state: `getUser()`, `getSettings()`, `getIdentityConfig()`
|
|
566
590
|
|
|
567
|
-
Browser-side auth mutations call the
|
|
591
|
+
Browser-side auth mutations call the Identity API directly from the browser, set the `nf_jwt` cookie, and emit `onAuthChange` events. This keeps the client UI in sync immediately. Server-side reads work because the cookie is sent with every request.
|
|
568
592
|
|
|
569
593
|
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.
|
|
570
594
|
|
|
@@ -1010,14 +1034,16 @@ if (result?.type === 'invite' && result.token) {
|
|
|
1010
1034
|
|
|
1011
1035
|
### Session lifetime
|
|
1012
1036
|
|
|
1013
|
-
Sessions are managed by Netlify Identity
|
|
1037
|
+
Sessions are managed by Netlify Identity on the server side. The library stores two cookies:
|
|
1014
1038
|
|
|
1015
|
-
- **`nf_jwt`**: A short-lived JWT access token (default: 1 hour).
|
|
1039
|
+
- **`nf_jwt`**: A short-lived JWT access token (default: 1 hour).
|
|
1016
1040
|
- **`nf_refresh`**: A long-lived refresh token used to obtain new access tokens without re-authenticating.
|
|
1017
1041
|
|
|
1018
|
-
|
|
1042
|
+
**Browser auto-refresh:** After `login()`, `hydrateSession()`, or `handleAuthCallback()` establishes a session, the library automatically schedules a background refresh 60 seconds before the access token expires. When the refresh fires, it obtains a new access token, syncs it to the `nf_jwt` cookie, and emits a `TOKEN_REFRESH` event. This keeps the cookie fresh as long as the user has the tab open. If the refresh fails (e.g., the refresh token was revoked), the timer stops and the user will need to log in again.
|
|
1043
|
+
|
|
1044
|
+
**Server-side refresh:** On the server, the access token in the `nf_jwt` cookie is validated as-is. If it has expired and no refresh happens, `getUser()` returns `null`. To handle this, call `refreshSession()` in your framework middleware or request handler. This checks if the token is near expiry, exchanges the refresh token for a new one, and updates the cookies on the response.
|
|
1019
1045
|
|
|
1020
|
-
Session lifetime is configured in your
|
|
1046
|
+
Session lifetime is configured in your Netlify Identity settings, not in this library.
|
|
1021
1047
|
|
|
1022
1048
|
## License
|
|
1023
1049
|
|
package/dist/index.cjs
CHANGED
|
@@ -47,6 +47,7 @@ __export(index_exports, {
|
|
|
47
47
|
oauthLogin: () => oauthLogin,
|
|
48
48
|
onAuthChange: () => onAuthChange,
|
|
49
49
|
recoverPassword: () => recoverPassword,
|
|
50
|
+
refreshSession: () => refreshSession,
|
|
50
51
|
requestPasswordRecovery: () => requestPasswordRecovery,
|
|
51
52
|
signup: () => signup,
|
|
52
53
|
updateUser: () => updateUser,
|
|
@@ -271,6 +272,90 @@ var onAuthChange = (callback) => {
|
|
|
271
272
|
};
|
|
272
273
|
};
|
|
273
274
|
|
|
275
|
+
// src/refresh.ts
|
|
276
|
+
var REFRESH_MARGIN_S = 60;
|
|
277
|
+
var refreshTimer = null;
|
|
278
|
+
var startTokenRefresh = () => {
|
|
279
|
+
if (!isBrowser()) return;
|
|
280
|
+
stopTokenRefresh();
|
|
281
|
+
const client = getGoTrueClient();
|
|
282
|
+
const user = client?.currentUser();
|
|
283
|
+
if (!user) return;
|
|
284
|
+
const token = user.tokenDetails();
|
|
285
|
+
if (!token?.expires_at) return;
|
|
286
|
+
const nowS = Math.floor(Date.now() / 1e3);
|
|
287
|
+
const expiresAtS = typeof token.expires_at === "number" && token.expires_at > 1e12 ? Math.floor(token.expires_at / 1e3) : token.expires_at;
|
|
288
|
+
const delayMs = Math.max(0, expiresAtS - nowS - REFRESH_MARGIN_S) * 1e3;
|
|
289
|
+
refreshTimer = setTimeout(async () => {
|
|
290
|
+
try {
|
|
291
|
+
const freshJwt = await user.jwt(true);
|
|
292
|
+
const freshDetails = user.tokenDetails();
|
|
293
|
+
setBrowserAuthCookies(freshJwt, freshDetails?.refresh_token);
|
|
294
|
+
emitAuthEvent(AUTH_EVENTS.TOKEN_REFRESH, toUser(user));
|
|
295
|
+
startTokenRefresh();
|
|
296
|
+
} catch {
|
|
297
|
+
stopTokenRefresh();
|
|
298
|
+
}
|
|
299
|
+
}, delayMs);
|
|
300
|
+
};
|
|
301
|
+
var stopTokenRefresh = () => {
|
|
302
|
+
if (refreshTimer !== null) {
|
|
303
|
+
clearTimeout(refreshTimer);
|
|
304
|
+
refreshTimer = null;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
var refreshSession = async () => {
|
|
308
|
+
if (isBrowser()) {
|
|
309
|
+
const client = getGoTrueClient();
|
|
310
|
+
const user = client?.currentUser();
|
|
311
|
+
if (!user) return null;
|
|
312
|
+
try {
|
|
313
|
+
const jwt = await user.jwt();
|
|
314
|
+
setBrowserAuthCookies(jwt, user.tokenDetails()?.refresh_token);
|
|
315
|
+
return jwt;
|
|
316
|
+
} catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const accessToken = getServerCookie(NF_JWT_COOKIE);
|
|
321
|
+
const refreshToken = getServerCookie(NF_REFRESH_COOKIE);
|
|
322
|
+
if (!accessToken || !refreshToken) return null;
|
|
323
|
+
const decoded = decodeJwtPayload(accessToken);
|
|
324
|
+
if (!decoded?.exp) return null;
|
|
325
|
+
const nowS = Math.floor(Date.now() / 1e3);
|
|
326
|
+
if (decoded.exp - nowS > REFRESH_MARGIN_S) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
const ctx = getIdentityContext();
|
|
330
|
+
const identityUrl = ctx?.url ?? (globalThis.Netlify?.context?.url ? new URL(IDENTITY_PATH, globalThis.Netlify.context.url).href : null);
|
|
331
|
+
if (!identityUrl) {
|
|
332
|
+
throw new AuthError("Could not determine the Identity endpoint URL for token refresh");
|
|
333
|
+
}
|
|
334
|
+
let res;
|
|
335
|
+
try {
|
|
336
|
+
res = await fetch(`${identityUrl}/token`, {
|
|
337
|
+
method: "POST",
|
|
338
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
339
|
+
body: `grant_type=refresh_token&refresh_token=${refreshToken}`
|
|
340
|
+
});
|
|
341
|
+
} catch (error) {
|
|
342
|
+
throw AuthError.from(error);
|
|
343
|
+
}
|
|
344
|
+
if (!res.ok) {
|
|
345
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
346
|
+
if (res.status === 401 || res.status === 400) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
throw new AuthError(errorBody.msg || `Token refresh failed (${res.status})`, res.status);
|
|
350
|
+
}
|
|
351
|
+
const data = await res.json();
|
|
352
|
+
const cookies = globalThis.Netlify?.context?.cookies;
|
|
353
|
+
if (cookies) {
|
|
354
|
+
setAuthCookies(cookies, data.access_token, data.refresh_token);
|
|
355
|
+
}
|
|
356
|
+
return data.access_token;
|
|
357
|
+
};
|
|
358
|
+
|
|
274
359
|
// src/auth.ts
|
|
275
360
|
var getCookies = () => {
|
|
276
361
|
const cookies = globalThis.Netlify?.context?.cookies;
|
|
@@ -335,6 +420,7 @@ var login = async (email, password) => {
|
|
|
335
420
|
const jwt = await gotrueUser.jwt();
|
|
336
421
|
setBrowserAuthCookies(jwt, gotrueUser.tokenDetails()?.refresh_token);
|
|
337
422
|
const user = toUser(gotrueUser);
|
|
423
|
+
startTokenRefresh();
|
|
338
424
|
emitAuthEvent(AUTH_EVENTS.LOGIN, user);
|
|
339
425
|
return user;
|
|
340
426
|
} catch (error) {
|
|
@@ -379,6 +465,7 @@ var signup = async (email, password, data) => {
|
|
|
379
465
|
const refreshToken = response.tokenDetails?.()?.refresh_token;
|
|
380
466
|
setBrowserAuthCookies(jwt, refreshToken);
|
|
381
467
|
}
|
|
468
|
+
startTokenRefresh();
|
|
382
469
|
emitAuthEvent(AUTH_EVENTS.LOGIN, user);
|
|
383
470
|
}
|
|
384
471
|
return user;
|
|
@@ -410,6 +497,7 @@ var logout = async () => {
|
|
|
410
497
|
await currentUser.logout();
|
|
411
498
|
}
|
|
412
499
|
deleteBrowserAuthCookies();
|
|
500
|
+
stopTokenRefresh();
|
|
413
501
|
emitAuthEvent(AUTH_EVENTS.LOGOUT, null);
|
|
414
502
|
} catch (error) {
|
|
415
503
|
throw AuthError.from(error);
|
|
@@ -462,6 +550,7 @@ var handleOAuthCallback = async (client, params, accessToken) => {
|
|
|
462
550
|
);
|
|
463
551
|
setBrowserAuthCookies(accessToken, refreshToken || void 0);
|
|
464
552
|
const user = toUser(gotrueUser);
|
|
553
|
+
startTokenRefresh();
|
|
465
554
|
clearHash();
|
|
466
555
|
emitAuthEvent(AUTH_EVENTS.LOGIN, user);
|
|
467
556
|
return { type: "oauth", user };
|
|
@@ -471,6 +560,7 @@ var handleConfirmationCallback = async (client, token) => {
|
|
|
471
560
|
const jwt = await gotrueUser.jwt();
|
|
472
561
|
setBrowserAuthCookies(jwt, gotrueUser.tokenDetails()?.refresh_token);
|
|
473
562
|
const user = toUser(gotrueUser);
|
|
563
|
+
startTokenRefresh();
|
|
474
564
|
clearHash();
|
|
475
565
|
emitAuthEvent(AUTH_EVENTS.LOGIN, user);
|
|
476
566
|
return { type: "confirmation", user };
|
|
@@ -480,6 +570,7 @@ var handleRecoveryCallback = async (client, token) => {
|
|
|
480
570
|
const jwt = await gotrueUser.jwt();
|
|
481
571
|
setBrowserAuthCookies(jwt, gotrueUser.tokenDetails()?.refresh_token);
|
|
482
572
|
const user = toUser(gotrueUser);
|
|
573
|
+
startTokenRefresh();
|
|
483
574
|
clearHash();
|
|
484
575
|
emitAuthEvent(AUTH_EVENTS.RECOVERY, user);
|
|
485
576
|
return { type: "recovery", user };
|
|
@@ -547,6 +638,7 @@ var hydrateSession = async () => {
|
|
|
547
638
|
return null;
|
|
548
639
|
}
|
|
549
640
|
const user = toUser(gotrueUser);
|
|
641
|
+
startTokenRefresh();
|
|
550
642
|
emitAuthEvent(AUTH_EVENTS.LOGIN, user);
|
|
551
643
|
return user;
|
|
552
644
|
};
|
|
@@ -943,6 +1035,7 @@ var admin = { listUsers, getUser: getUser2, createUser, updateUser: updateUser2,
|
|
|
943
1035
|
oauthLogin,
|
|
944
1036
|
onAuthChange,
|
|
945
1037
|
recoverPassword,
|
|
1038
|
+
refreshSession,
|
|
946
1039
|
requestPasswordRecovery,
|
|
947
1040
|
signup,
|
|
948
1041
|
updateUser,
|