@netlify/identity 0.4.1 → 0.4.2
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 +29 -15
- package/dist/index.cjs +63 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -47
- package/dist/index.d.ts +34 -47
- package/dist/index.js +63 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ This library provides a unified API that works in both browser and server contex
|
|
|
38
38
|
- [Password recovery](#password-recovery)
|
|
39
39
|
- [Invite acceptance](#invite-acceptance)
|
|
40
40
|
- [Session lifetime](#session-lifetime)
|
|
41
|
+
- [Caching and authenticated content](#caching-and-authenticated-content)
|
|
41
42
|
|
|
42
43
|
## Installation
|
|
43
44
|
|
|
@@ -302,10 +303,9 @@ Updates the current user's metadata or credentials. Requires an active session.
|
|
|
302
303
|
|
|
303
304
|
### Admin Operations
|
|
304
305
|
|
|
305
|
-
The `admin` namespace provides user management functions
|
|
306
|
+
The `admin` namespace provides server-only user management functions. Admin methods use the operator token from the Netlify runtime, which is automatically available in Netlify Functions and Edge Functions.
|
|
306
307
|
|
|
307
|
-
|
|
308
|
-
- **Browser:** Uses the logged-in user's JWT. The user must have an admin role.
|
|
308
|
+
Calling any admin method from a browser environment throws an `AuthError`.
|
|
309
309
|
|
|
310
310
|
```ts
|
|
311
311
|
import { admin } from '@netlify/identity'
|
|
@@ -341,9 +341,9 @@ export default async (req: Request, context: Context) => {
|
|
|
341
341
|
admin.listUsers(options?: ListUsersOptions): Promise<User[]>
|
|
342
342
|
```
|
|
343
343
|
|
|
344
|
-
Lists all users. Pagination options
|
|
344
|
+
Lists all users. Pagination options (`page`, `perPage`) are forwarded as query parameters.
|
|
345
345
|
|
|
346
|
-
**Throws:** `AuthError` if
|
|
346
|
+
**Throws:** `AuthError` if called from a browser, or if the operator token is missing.
|
|
347
347
|
|
|
348
348
|
#### `admin.getUser`
|
|
349
349
|
|
|
@@ -353,7 +353,7 @@ admin.getUser(userId: string): Promise<User>
|
|
|
353
353
|
|
|
354
354
|
Gets a single user by ID.
|
|
355
355
|
|
|
356
|
-
**Throws:** `AuthError` if the user is not found, the operator token is missing
|
|
356
|
+
**Throws:** `AuthError` if called from a browser, the user is not found, or the operator token is missing.
|
|
357
357
|
|
|
358
358
|
#### `admin.createUser`
|
|
359
359
|
|
|
@@ -361,9 +361,9 @@ Gets a single user by ID.
|
|
|
361
361
|
admin.createUser(params: CreateUserParams): Promise<User>
|
|
362
362
|
```
|
|
363
363
|
|
|
364
|
-
Creates a new user. The user is auto-confirmed. Optional `data`
|
|
364
|
+
Creates a new user. The user is auto-confirmed. Optional `data` forwards allowed fields (`role`, `aud`, `app_metadata`, `user_metadata`) to the request body. Other keys are silently ignored. `data` cannot override `email`, `password`, or `confirm`.
|
|
365
365
|
|
|
366
|
-
**Throws:** `AuthError`
|
|
366
|
+
**Throws:** `AuthError` if called from a browser, the email already exists, or the operator token is missing.
|
|
367
367
|
|
|
368
368
|
#### `admin.updateUser`
|
|
369
369
|
|
|
@@ -371,9 +371,9 @@ Creates a new user. The user is auto-confirmed. Optional `data` is spread into t
|
|
|
371
371
|
admin.updateUser(userId: string, attributes: AdminUserUpdates): Promise<User>
|
|
372
372
|
```
|
|
373
373
|
|
|
374
|
-
Updates an existing user by ID.
|
|
374
|
+
Updates an existing user by ID. Only typed `AdminUserUpdates` fields are forwarded (e.g., `{ email: 'new@example.com' }`, `{ role: 'editor' }`).
|
|
375
375
|
|
|
376
|
-
**Throws:** `AuthError` if the user is not found or the update fails.
|
|
376
|
+
**Throws:** `AuthError` if called from a browser, the user is not found, or the update fails.
|
|
377
377
|
|
|
378
378
|
#### `admin.deleteUser`
|
|
379
379
|
|
|
@@ -383,7 +383,7 @@ admin.deleteUser(userId: string): Promise<void>
|
|
|
383
383
|
|
|
384
384
|
Deletes a user by ID.
|
|
385
385
|
|
|
386
|
-
**Throws:** `AuthError` if the user is not found or the deletion fails.
|
|
386
|
+
**Throws:** `AuthError` if called from a browser, the user is not found, or the deletion fails.
|
|
387
387
|
|
|
388
388
|
### Types
|
|
389
389
|
|
|
@@ -451,14 +451,14 @@ interface AdminUserUpdates {
|
|
|
451
451
|
email?: string
|
|
452
452
|
password?: string
|
|
453
453
|
role?: string
|
|
454
|
+
aud?: string
|
|
454
455
|
confirm?: boolean
|
|
455
456
|
app_metadata?: Record<string, unknown>
|
|
456
457
|
user_metadata?: Record<string, unknown>
|
|
457
|
-
[key: string]: unknown
|
|
458
458
|
}
|
|
459
459
|
```
|
|
460
460
|
|
|
461
|
-
Fields accepted by `admin.updateUser()`. Unlike `UserUpdates`, admin updates can set `role`, force-confirm a user, and write to `app_metadata`.
|
|
461
|
+
Fields accepted by `admin.updateUser()`. Unlike `UserUpdates`, admin updates can set `role`, `aud`, force-confirm a user, and write to `app_metadata`. Only these typed fields are forwarded.
|
|
462
462
|
|
|
463
463
|
#### `SignupData`
|
|
464
464
|
|
|
@@ -487,7 +487,7 @@ interface ListUsersOptions {
|
|
|
487
487
|
}
|
|
488
488
|
```
|
|
489
489
|
|
|
490
|
-
Pagination options for `admin.listUsers()`.
|
|
490
|
+
Pagination options for `admin.listUsers()`.
|
|
491
491
|
|
|
492
492
|
#### `CreateUserParams`
|
|
493
493
|
|
|
@@ -499,7 +499,7 @@ interface CreateUserParams {
|
|
|
499
499
|
}
|
|
500
500
|
```
|
|
501
501
|
|
|
502
|
-
Parameters for `admin.createUser()`. Optional `data`
|
|
502
|
+
Parameters for `admin.createUser()`. Optional `data` forwards allowed fields (`role`, `aud`, `app_metadata`, `user_metadata`) to the request body. Other keys are silently ignored.
|
|
503
503
|
|
|
504
504
|
#### `Admin`
|
|
505
505
|
|
|
@@ -1048,6 +1048,20 @@ Sessions are managed by Netlify Identity on the server side. The library stores
|
|
|
1048
1048
|
|
|
1049
1049
|
Session lifetime is configured in your Netlify Identity settings, not in this library.
|
|
1050
1050
|
|
|
1051
|
+
### Caching and authenticated content
|
|
1052
|
+
|
|
1053
|
+
Pages that display user-specific data (names, emails, roles, account settings) should not be served from a shared cache. If a cache stores an authenticated response and serves it to a different user, that user sees someone else's data. This applies to any authentication system, not just Netlify Identity.
|
|
1054
|
+
|
|
1055
|
+
**Next.js App Router** has multiple caching layers that are active by default:
|
|
1056
|
+
|
|
1057
|
+
- **Static rendering:** Server Components are statically rendered at build time unless they call a [Dynamic API](https://nextjs.org/docs/app/guides/caching#dynamic-rendering) like `cookies()`. This library's `getUser()` already calls `headers()` internally to opt the route into dynamic rendering, but if you check auth state without calling `getUser()` (e.g., reading the `nf_jwt` cookie directly), the page may still be statically cached. Always use `getUser()` rather than reading cookies directly.
|
|
1058
|
+
- **ISR (Incremental Static Regeneration):** Do not use ISR for pages that display user-specific content. ISR regenerates the page for the first visitor after the revalidation window and caches the result for all subsequent visitors.
|
|
1059
|
+
- **`use cache` / `unstable_cache`:** These directives cannot access `cookies()` or `headers()` directly. If you need to cache part of an authenticated page, read cookies outside the cache scope and pass relevant values as arguments.
|
|
1060
|
+
|
|
1061
|
+
> **Note:** Next.js caching defaults have changed across versions. For example, [Next.js 15 changed `fetch` requests, `GET` Route Handlers, and the client Router Cache to be uncached by default](https://nextjs.org/blog/next-15#caching-semantics), reversing the previous opt-out model. Check the [caching guide](https://nextjs.org/docs/app/guides/caching) for your specific Next.js version.
|
|
1062
|
+
|
|
1063
|
+
**Other SSR frameworks (Remix, Astro, SvelteKit, TanStack Start):** These frameworks do not cache SSR responses by default. If you add caching headers to improve performance, exclude routes that call `getUser()` or read auth cookies.
|
|
1064
|
+
|
|
1051
1065
|
## License
|
|
1052
1066
|
|
|
1053
1067
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -926,6 +926,19 @@ var updateUser = async (updates) => {
|
|
|
926
926
|
};
|
|
927
927
|
|
|
928
928
|
// src/admin.ts
|
|
929
|
+
var SERVER_ONLY_MESSAGE = "Admin operations are server-only. Call admin methods from a Netlify Function or Edge Function, not from browser code.";
|
|
930
|
+
var sanitizeUserId = (userId) => {
|
|
931
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
932
|
+
if (!uuidRegex.test(userId)) {
|
|
933
|
+
throw new AuthError("User ID is not a valid UUID");
|
|
934
|
+
}
|
|
935
|
+
return encodeURIComponent(userId);
|
|
936
|
+
};
|
|
937
|
+
var assertServer = () => {
|
|
938
|
+
if (isBrowser()) {
|
|
939
|
+
throw new AuthError(SERVER_ONLY_MESSAGE);
|
|
940
|
+
}
|
|
941
|
+
};
|
|
929
942
|
var getAdminAuth = () => {
|
|
930
943
|
const ctx = getIdentityContext();
|
|
931
944
|
if (!ctx?.url) {
|
|
@@ -957,105 +970,67 @@ var adminFetch = async (path, options = {}) => {
|
|
|
957
970
|
}
|
|
958
971
|
return res;
|
|
959
972
|
};
|
|
960
|
-
var getAdminUser = () => {
|
|
961
|
-
const client = getClient();
|
|
962
|
-
const user = client.currentUser();
|
|
963
|
-
if (!user) {
|
|
964
|
-
throw new AuthError("Admin operations require a logged-in user with admin role");
|
|
965
|
-
}
|
|
966
|
-
return user;
|
|
967
|
-
};
|
|
968
973
|
var listUsers = async (options) => {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
}
|
|
979
|
-
try {
|
|
980
|
-
const user = getAdminUser();
|
|
981
|
-
const users = await user.admin.listUsers("");
|
|
982
|
-
return users.map(toUser);
|
|
983
|
-
} catch (error) {
|
|
984
|
-
if (error instanceof AuthError) throw error;
|
|
985
|
-
throw new AuthError(error.message, void 0, { cause: error });
|
|
986
|
-
}
|
|
974
|
+
assertServer();
|
|
975
|
+
const params = new URLSearchParams();
|
|
976
|
+
if (options?.page != null) params.set("page", String(options.page));
|
|
977
|
+
if (options?.perPage != null) params.set("per_page", String(options.perPage));
|
|
978
|
+
const query = params.toString();
|
|
979
|
+
const path = `/admin/users${query ? `?${query}` : ""}`;
|
|
980
|
+
const res = await adminFetch(path);
|
|
981
|
+
const body = await res.json();
|
|
982
|
+
return body.users.map(toUser);
|
|
987
983
|
};
|
|
988
984
|
var getUser2 = async (userId) => {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
try {
|
|
995
|
-
const user = getAdminUser();
|
|
996
|
-
const userData = await user.admin.getUser({ id: userId });
|
|
997
|
-
return toUser(userData);
|
|
998
|
-
} catch (error) {
|
|
999
|
-
if (error instanceof AuthError) throw error;
|
|
1000
|
-
throw new AuthError(error.message, void 0, { cause: error });
|
|
1001
|
-
}
|
|
985
|
+
assertServer();
|
|
986
|
+
const sanitizedUserId = sanitizeUserId(userId);
|
|
987
|
+
const res = await adminFetch(`/admin/users/${sanitizedUserId}`);
|
|
988
|
+
const userData = await res.json();
|
|
989
|
+
return toUser(userData);
|
|
1002
990
|
};
|
|
1003
991
|
var createUser = async (params) => {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
try {
|
|
1018
|
-
const user = getAdminUser();
|
|
1019
|
-
const userData = await user.admin.createUser(params.email, params.password, {
|
|
1020
|
-
...params.data,
|
|
1021
|
-
confirm: true
|
|
1022
|
-
});
|
|
1023
|
-
return toUser(userData);
|
|
1024
|
-
} catch (error) {
|
|
1025
|
-
if (error instanceof AuthError) throw error;
|
|
1026
|
-
throw new AuthError(error.message, void 0, { cause: error });
|
|
992
|
+
assertServer();
|
|
993
|
+
const body = {
|
|
994
|
+
email: params.email,
|
|
995
|
+
password: params.password,
|
|
996
|
+
confirm: true
|
|
997
|
+
};
|
|
998
|
+
if (params.data) {
|
|
999
|
+
const allowedKeys = ["role", "aud", "app_metadata", "user_metadata"];
|
|
1000
|
+
for (const key of allowedKeys) {
|
|
1001
|
+
if (key in params.data) {
|
|
1002
|
+
body[key] = params.data[key];
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1027
1005
|
}
|
|
1006
|
+
const res = await adminFetch("/admin/users", {
|
|
1007
|
+
method: "POST",
|
|
1008
|
+
body: JSON.stringify(body)
|
|
1009
|
+
});
|
|
1010
|
+
const userData = await res.json();
|
|
1011
|
+
return toUser(userData);
|
|
1028
1012
|
};
|
|
1029
1013
|
var updateUser2 = async (userId, attributes) => {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
try {
|
|
1039
|
-
const user = getAdminUser();
|
|
1040
|
-
const userData = await user.admin.updateUser({ id: userId }, attributes);
|
|
1041
|
-
return toUser(userData);
|
|
1042
|
-
} catch (error) {
|
|
1043
|
-
if (error instanceof AuthError) throw error;
|
|
1044
|
-
throw new AuthError(error.message, void 0, { cause: error });
|
|
1014
|
+
assertServer();
|
|
1015
|
+
const sanitizedUserId = sanitizeUserId(userId);
|
|
1016
|
+
const body = {};
|
|
1017
|
+
const allowedKeys = ["email", "password", "role", "aud", "confirm", "app_metadata", "user_metadata"];
|
|
1018
|
+
for (const key of allowedKeys) {
|
|
1019
|
+
if (key in attributes) {
|
|
1020
|
+
body[key] = attributes[key];
|
|
1021
|
+
}
|
|
1045
1022
|
}
|
|
1023
|
+
const res = await adminFetch(`/admin/users/${sanitizedUserId}`, {
|
|
1024
|
+
method: "PUT",
|
|
1025
|
+
body: JSON.stringify(body)
|
|
1026
|
+
});
|
|
1027
|
+
const userData = await res.json();
|
|
1028
|
+
return toUser(userData);
|
|
1046
1029
|
};
|
|
1047
1030
|
var deleteUser = async (userId) => {
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1052
|
-
try {
|
|
1053
|
-
const user = getAdminUser();
|
|
1054
|
-
await user.admin.deleteUser({ id: userId });
|
|
1055
|
-
} catch (error) {
|
|
1056
|
-
if (error instanceof AuthError) throw error;
|
|
1057
|
-
throw new AuthError(error.message, void 0, { cause: error });
|
|
1058
|
-
}
|
|
1031
|
+
assertServer();
|
|
1032
|
+
const sanitizedUserId = sanitizeUserId(userId);
|
|
1033
|
+
await adminFetch(`/admin/users/${sanitizedUserId}`, { method: "DELETE" });
|
|
1059
1034
|
};
|
|
1060
1035
|
var admin = { listUsers, getUser: getUser2, createUser, updateUser: updateUser2, deleteUser };
|
|
1061
1036
|
// Annotate the CommonJS export names for ESM import in node:
|