@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 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 for administrators. These work in two contexts:
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
- - **Server:** Uses the operator token from the Netlify runtime for full admin access. No logged-in user required.
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 are supported on the server; they are ignored in the browser.
344
+ Lists all users. Pagination options (`page`, `perPage`) are forwarded as query parameters.
345
345
 
346
- **Throws:** `AuthError` if the operator token is missing (server) or no user is logged in (browser).
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 (server), or no user is logged in (browser).
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` is spread into the request body as additional attributes.
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` on failure (e.g., email already exists).
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. Pass any attributes to change (e.g., `{ email: 'new@example.com' }`). See `AdminUserUpdates` for typed fields.
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()`. Only used on the server; pagination is ignored in the browser.
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` is spread into the request body as top-level attributes (use it to set `app_metadata`, `user_metadata`, `role`, etc.).
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
- if (!isBrowser()) {
970
- const params = new URLSearchParams();
971
- if (options?.page != null) params.set("page", String(options.page));
972
- if (options?.perPage != null) params.set("per_page", String(options.perPage));
973
- const query = params.toString();
974
- const path = `/admin/users${query ? `?${query}` : ""}`;
975
- const res = await adminFetch(path);
976
- const body = await res.json();
977
- return body.users.map(toUser);
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
- if (!isBrowser()) {
990
- const res = await adminFetch(`/admin/users/${userId}`);
991
- const userData = await res.json();
992
- return toUser(userData);
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
- if (!isBrowser()) {
1005
- const res = await adminFetch("/admin/users", {
1006
- method: "POST",
1007
- body: JSON.stringify({
1008
- email: params.email,
1009
- password: params.password,
1010
- ...params.data,
1011
- confirm: true
1012
- })
1013
- });
1014
- const userData = await res.json();
1015
- return toUser(userData);
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
- if (!isBrowser()) {
1031
- const res = await adminFetch(`/admin/users/${userId}`, {
1032
- method: "PUT",
1033
- body: JSON.stringify(attributes)
1034
- });
1035
- const userData = await res.json();
1036
- return toUser(userData);
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
- if (!isBrowser()) {
1049
- await adminFetch(`/admin/users/${userId}`, { method: "DELETE" });
1050
- return;
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: