@netlify/identity 0.1.1-alpha.22 → 0.1.1-alpha.24

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
@@ -164,7 +164,7 @@ Processes the URL hash after an OAuth redirect, email confirmation, password rec
164
164
  onAuthChange(callback: AuthCallback): () => void
165
165
  ```
166
166
 
167
- Subscribes to auth state changes (login, logout, token refresh, user updates). Returns an unsubscribe function. Also fires on cross-tab session changes. No-op on the server.
167
+ Subscribes to auth state changes (login, logout, token refresh, user updates, and recovery). Returns an unsubscribe function. Also fires on cross-tab session changes. No-op on the server. The `'recovery'` event fires when `handleAuthCallback()` processes a password recovery token; listen for it to redirect users to a password reset form.
168
168
 
169
169
  #### `hydrateSession`
170
170
 
@@ -323,10 +323,24 @@ interface AppMetadata {
323
323
  }
324
324
  ```
325
325
 
326
+ #### `AUTH_EVENTS`
327
+
328
+ ```ts
329
+ const AUTH_EVENTS: {
330
+ LOGIN: 'login'
331
+ LOGOUT: 'logout'
332
+ TOKEN_REFRESH: 'token_refresh'
333
+ USER_UPDATED: 'user_updated'
334
+ RECOVERY: 'recovery'
335
+ }
336
+ ```
337
+
338
+ Constants for auth event names. Use these instead of string literals for type safety and autocomplete.
339
+
326
340
  #### `AuthEvent`
327
341
 
328
342
  ```ts
329
- type AuthEvent = 'login' | 'logout' | 'token_refresh' | 'user_updated'
343
+ type AuthEvent = 'login' | 'logout' | 'token_refresh' | 'user_updated' | 'recovery'
330
344
  ```
331
345
 
332
346
  #### `AuthCallback`
@@ -646,6 +660,8 @@ export function CallbackHandler({ children }: { children: React.ReactNode }) {
646
660
  }
647
661
  if (result.type === 'invite') {
648
662
  window.location.href = `/accept-invite?token=${result.token}`
663
+ } else if (result.type === 'recovery') {
664
+ window.location.href = '/reset-password'
649
665
  } else {
650
666
  window.location.href = '/dashboard'
651
667
  }
@@ -704,25 +720,29 @@ function NavBar() {
704
720
 
705
721
  ### Listening for auth changes
706
722
 
707
- 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`).
723
+ Use `onAuthChange` to keep your UI in sync with auth state. It fires on login, logout, token refresh, user updates, and recovery. It also detects session changes in other browser tabs (via `localStorage`).
708
724
 
709
725
  ```ts
710
- import { onAuthChange } from '@netlify/identity'
726
+ import { onAuthChange, AUTH_EVENTS } from '@netlify/identity'
711
727
 
712
728
  const unsubscribe = onAuthChange((event, user) => {
713
729
  switch (event) {
714
- case 'login':
730
+ case AUTH_EVENTS.LOGIN:
715
731
  console.log('Logged in:', user?.email)
716
732
  break
717
- case 'logout':
733
+ case AUTH_EVENTS.LOGOUT:
718
734
  console.log('Logged out')
719
735
  break
720
- case 'token_refresh':
736
+ case AUTH_EVENTS.TOKEN_REFRESH:
721
737
  console.log('Token refreshed for:', user?.email)
722
738
  break
723
- case 'user_updated':
739
+ case AUTH_EVENTS.USER_UPDATED:
724
740
  console.log('User updated:', user?.email)
725
741
  break
742
+ case AUTH_EVENTS.RECOVERY:
743
+ console.log('Recovery login:', user?.email)
744
+ // Redirect to password reset form, then call updateUser({ password })
745
+ break
726
746
  }
727
747
  })
728
748
 
@@ -755,11 +775,11 @@ if (result?.type === 'oauth') {
755
775
  }
756
776
  ```
757
777
 
758
- `handleAuthCallback()` exchanges the token in the URL hash, logs the user in, clears the hash, and emits a `'login'` event via `onAuthChange`.
778
+ `handleAuthCallback()` exchanges the token in the URL hash, logs the user in, clears the hash, and emits an auth event via `onAuthChange` (`'login'` for OAuth/confirmation, `'recovery'` for password recovery).
759
779
 
760
780
  ### Password recovery
761
781
 
762
- 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.
782
+ 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}`. A `'recovery'` event (not `'login'`) is emitted via `onAuthChange`, so event-based listeners can also detect this flow. You then show a "set new password" form and call `updateUser()` to save it.
763
783
 
764
784
  **Step by step:**
765
785
 
@@ -780,6 +800,19 @@ if (result?.type === 'recovery') {
780
800
  }
781
801
  ```
782
802
 
803
+ If you use the event-based pattern instead of checking `result.type`, listen for the `'recovery'` event:
804
+
805
+ ```ts
806
+ import { onAuthChange, AUTH_EVENTS } from '@netlify/identity'
807
+
808
+ onAuthChange((event, user) => {
809
+ if (event === AUTH_EVENTS.RECOVERY) {
810
+ // Redirect to password reset form.
811
+ // The user is authenticated, so call updateUser({ password }) to set the new password.
812
+ }
813
+ })
814
+ ```
815
+
783
816
  ### Invite acceptance
784
817
 
785
818
  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.
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AUTH_EVENTS: () => AUTH_EVENTS,
33
34
  AuthError: () => AuthError,
34
35
  MissingIdentityError: () => MissingIdentityError,
35
36
  acceptInvite: () => acceptInvite,
@@ -70,7 +71,7 @@ var AuthError = class extends Error {
70
71
  }
71
72
  };
72
73
  var MissingIdentityError = class extends Error {
73
- constructor(message = "Netlify Identity is not available. Enable Identity in your site dashboard and use `netlify dev` for local development.") {
74
+ constructor(message = "Netlify Identity is not available.") {
74
75
  super(message);
75
76
  this.name = "MissingIdentityError";
76
77
  }
@@ -348,22 +349,15 @@ var getSettings = async () => {
348
349
  }
349
350
  };
350
351
 
351
- // src/auth.ts
352
- var getCookies = () => {
353
- const cookies = globalThis.Netlify?.context?.cookies;
354
- if (!cookies) {
355
- throw new AuthError("Server-side auth requires Netlify Functions runtime");
356
- }
357
- return cookies;
358
- };
359
- var getServerIdentityUrl = () => {
360
- const ctx = getIdentityContext();
361
- if (!ctx?.url) {
362
- throw new AuthError("Could not determine the Identity endpoint URL on the server");
363
- }
364
- return ctx.url;
352
+ // src/events.ts
353
+ var AUTH_EVENTS = {
354
+ LOGIN: "login",
355
+ LOGOUT: "logout",
356
+ TOKEN_REFRESH: "token_refresh",
357
+ USER_UPDATED: "user_updated",
358
+ RECOVERY: "recovery"
365
359
  };
366
- var persistSession = true;
360
+ var GOTRUE_STORAGE_KEY = "gotrue.user";
367
361
  var listeners = /* @__PURE__ */ new Set();
368
362
  var emitAuthEvent = (event, user) => {
369
363
  for (const listener of listeners) {
@@ -373,19 +367,18 @@ var emitAuthEvent = (event, user) => {
373
367
  }
374
368
  }
375
369
  };
376
- var GOTRUE_STORAGE_KEY = "gotrue.user";
377
370
  var storageListenerAttached = false;
378
371
  var attachStorageListener = () => {
379
- if (storageListenerAttached) return;
372
+ if (storageListenerAttached || !isBrowser()) return;
380
373
  storageListenerAttached = true;
381
374
  window.addEventListener("storage", (event) => {
382
375
  if (event.key !== GOTRUE_STORAGE_KEY) return;
383
376
  if (event.newValue) {
384
377
  const client = getGoTrueClient();
385
378
  const currentUser = client?.currentUser();
386
- emitAuthEvent("login", currentUser ? toUser(currentUser) : null);
379
+ emitAuthEvent(AUTH_EVENTS.LOGIN, currentUser ? toUser(currentUser) : null);
387
380
  } else {
388
- emitAuthEvent("logout", null);
381
+ emitAuthEvent(AUTH_EVENTS.LOGOUT, null);
389
382
  }
390
383
  });
391
384
  };
@@ -400,6 +393,23 @@ var onAuthChange = (callback) => {
400
393
  listeners.delete(callback);
401
394
  };
402
395
  };
396
+
397
+ // src/auth.ts
398
+ var getCookies = () => {
399
+ const cookies = globalThis.Netlify?.context?.cookies;
400
+ if (!cookies) {
401
+ throw new AuthError("Server-side auth requires Netlify Functions runtime");
402
+ }
403
+ return cookies;
404
+ };
405
+ var getServerIdentityUrl = () => {
406
+ const ctx = getIdentityContext();
407
+ if (!ctx?.url) {
408
+ throw new AuthError("Could not determine the Identity endpoint URL on the server");
409
+ }
410
+ return ctx.url;
411
+ };
412
+ var persistSession = true;
403
413
  var login = async (email, password) => {
404
414
  if (!isBrowser()) {
405
415
  const identityUrl = getServerIdentityUrl();
@@ -421,10 +431,7 @@ var login = async (email, password) => {
421
431
  }
422
432
  if (!res.ok) {
423
433
  const errorBody = await res.json().catch(() => ({}));
424
- throw new AuthError(
425
- errorBody.msg || errorBody.error_description || `Login failed (${res.status})`,
426
- res.status
427
- );
434
+ throw new AuthError(errorBody.msg || errorBody.error_description || `Login failed (${res.status})`, res.status);
428
435
  }
429
436
  const data = await res.json();
430
437
  const accessToken = data.access_token;
@@ -438,10 +445,7 @@ var login = async (email, password) => {
438
445
  }
439
446
  if (!userRes.ok) {
440
447
  const errorBody = await userRes.json().catch(() => ({}));
441
- throw new AuthError(
442
- errorBody.msg || `Failed to fetch user data (${userRes.status})`,
443
- userRes.status
444
- );
448
+ throw new AuthError(errorBody.msg || `Failed to fetch user data (${userRes.status})`, userRes.status);
445
449
  }
446
450
  const userData = await userRes.json();
447
451
  const user = toUser(userData);
@@ -454,7 +458,7 @@ var login = async (email, password) => {
454
458
  const jwt = await gotrueUser.jwt();
455
459
  setBrowserAuthCookies(jwt);
456
460
  const user = toUser(gotrueUser);
457
- emitAuthEvent("login", user);
461
+ emitAuthEvent(AUTH_EVENTS.LOGIN, user);
458
462
  return user;
459
463
  } catch (error) {
460
464
  throw new AuthError(error.message, void 0, { cause: error });
@@ -481,10 +485,9 @@ var signup = async (email, password, data) => {
481
485
  const responseData = await res.json();
482
486
  const user = toUser(responseData);
483
487
  if (responseData.confirmed_at) {
484
- const responseRecord = responseData;
485
- const accessToken = responseRecord.access_token;
488
+ const accessToken = responseData.access_token;
486
489
  if (accessToken) {
487
- setAuthCookies(cookies, accessToken, responseRecord.refresh_token);
490
+ setAuthCookies(cookies, accessToken, responseData.refresh_token);
488
491
  }
489
492
  }
490
493
  return user;
@@ -498,7 +501,7 @@ var signup = async (email, password, data) => {
498
501
  if (jwt) {
499
502
  setBrowserAuthCookies(jwt);
500
503
  }
501
- emitAuthEvent("login", user);
504
+ emitAuthEvent(AUTH_EVENTS.LOGIN, user);
502
505
  }
503
506
  return user;
504
507
  } catch (error) {
@@ -529,7 +532,7 @@ var logout = async () => {
529
532
  await currentUser.logout();
530
533
  }
531
534
  deleteBrowserAuthCookies();
532
- emitAuthEvent("logout", null);
535
+ emitAuthEvent(AUTH_EVENTS.LOGOUT, null);
533
536
  } catch (error) {
534
537
  throw new AuthError(error.message, void 0, { cause: error });
535
538
  }
@@ -547,87 +550,92 @@ var handleAuthCallback = async () => {
547
550
  const hash = window.location.hash.substring(1);
548
551
  if (!hash) return null;
549
552
  const client = getClient();
553
+ const params = new URLSearchParams(hash);
550
554
  try {
551
- const params = new URLSearchParams(hash);
552
555
  const accessToken = params.get("access_token");
553
- if (accessToken) {
554
- const refreshToken = params.get("refresh_token") ?? "";
555
- const gotrueUser = await client.createUser(
556
- {
557
- access_token: accessToken,
558
- token_type: params.get("token_type") ?? "bearer",
559
- expires_in: Number(params.get("expires_in")),
560
- expires_at: Number(params.get("expires_at")),
561
- refresh_token: refreshToken
562
- },
563
- persistSession
564
- );
565
- setBrowserAuthCookies(accessToken, refreshToken || void 0);
566
- const user = toUser(gotrueUser);
567
- clearHash();
568
- emitAuthEvent("login", user);
569
- return { type: "oauth", user };
570
- }
556
+ if (accessToken) return await handleOAuthCallback(client, params, accessToken);
571
557
  const confirmationToken = params.get("confirmation_token");
572
- if (confirmationToken) {
573
- const gotrueUser = await client.confirm(confirmationToken, persistSession);
574
- const jwt = await gotrueUser.jwt();
575
- setBrowserAuthCookies(jwt);
576
- const user = toUser(gotrueUser);
577
- clearHash();
578
- emitAuthEvent("login", user);
579
- return { type: "confirmation", user };
580
- }
558
+ if (confirmationToken) return await handleConfirmationCallback(client, confirmationToken);
581
559
  const recoveryToken = params.get("recovery_token");
582
- if (recoveryToken) {
583
- const gotrueUser = await client.recover(recoveryToken, persistSession);
584
- const jwt = await gotrueUser.jwt();
585
- setBrowserAuthCookies(jwt);
586
- const user = toUser(gotrueUser);
587
- clearHash();
588
- emitAuthEvent("login", user);
589
- return { type: "recovery", user };
590
- }
560
+ if (recoveryToken) return await handleRecoveryCallback(client, recoveryToken);
591
561
  const inviteToken = params.get("invite_token");
592
- if (inviteToken) {
593
- clearHash();
594
- return { type: "invite", user: null, token: inviteToken };
595
- }
562
+ if (inviteToken) return handleInviteCallback(inviteToken);
596
563
  const emailChangeToken = params.get("email_change_token");
597
- if (emailChangeToken) {
598
- const currentUser = client.currentUser();
599
- if (!currentUser) {
600
- throw new AuthError("Email change verification requires an active browser session");
601
- }
602
- const jwt = await currentUser.jwt();
603
- const identityUrl = `${window.location.origin}${IDENTITY_PATH}`;
604
- const emailChangeRes = await fetch(`${identityUrl}/user`, {
605
- method: "PUT",
606
- headers: {
607
- "Content-Type": "application/json",
608
- Authorization: `Bearer ${jwt}`
609
- },
610
- body: JSON.stringify({ email_change_token: emailChangeToken })
611
- });
612
- if (!emailChangeRes.ok) {
613
- const errorBody = await emailChangeRes.json().catch(() => ({}));
614
- throw new AuthError(
615
- errorBody.msg || `Email change verification failed (${emailChangeRes.status})`,
616
- emailChangeRes.status
617
- );
618
- }
619
- const emailChangeData = await emailChangeRes.json();
620
- const user = toUser(emailChangeData);
621
- clearHash();
622
- emitAuthEvent("user_updated", user);
623
- return { type: "email_change", user };
624
- }
564
+ if (emailChangeToken) return await handleEmailChangeCallback(client, emailChangeToken);
625
565
  return null;
626
566
  } catch (error) {
627
567
  if (error instanceof AuthError) throw error;
628
568
  throw new AuthError(error.message, void 0, { cause: error });
629
569
  }
630
570
  };
571
+ var handleOAuthCallback = async (client, params, accessToken) => {
572
+ const refreshToken = params.get("refresh_token") ?? "";
573
+ const gotrueUser = await client.createUser(
574
+ {
575
+ access_token: accessToken,
576
+ token_type: params.get("token_type") ?? "bearer",
577
+ expires_in: Number(params.get("expires_in")),
578
+ expires_at: Number(params.get("expires_at")),
579
+ refresh_token: refreshToken
580
+ },
581
+ persistSession
582
+ );
583
+ setBrowserAuthCookies(accessToken, refreshToken || void 0);
584
+ const user = toUser(gotrueUser);
585
+ clearHash();
586
+ emitAuthEvent(AUTH_EVENTS.LOGIN, user);
587
+ return { type: "oauth", user };
588
+ };
589
+ var handleConfirmationCallback = async (client, token) => {
590
+ const gotrueUser = await client.confirm(token, persistSession);
591
+ const jwt = await gotrueUser.jwt();
592
+ setBrowserAuthCookies(jwt);
593
+ const user = toUser(gotrueUser);
594
+ clearHash();
595
+ emitAuthEvent(AUTH_EVENTS.LOGIN, user);
596
+ return { type: "confirmation", user };
597
+ };
598
+ var handleRecoveryCallback = async (client, token) => {
599
+ const gotrueUser = await client.recover(token, persistSession);
600
+ const jwt = await gotrueUser.jwt();
601
+ setBrowserAuthCookies(jwt);
602
+ const user = toUser(gotrueUser);
603
+ clearHash();
604
+ emitAuthEvent(AUTH_EVENTS.RECOVERY, user);
605
+ return { type: "recovery", user };
606
+ };
607
+ var handleInviteCallback = (token) => {
608
+ clearHash();
609
+ return { type: "invite", user: null, token };
610
+ };
611
+ var handleEmailChangeCallback = async (client, emailChangeToken) => {
612
+ const currentUser = client.currentUser();
613
+ if (!currentUser) {
614
+ throw new AuthError("Email change verification requires an active browser session");
615
+ }
616
+ const jwt = await currentUser.jwt();
617
+ const identityUrl = `${window.location.origin}${IDENTITY_PATH}`;
618
+ const emailChangeRes = await fetch(`${identityUrl}/user`, {
619
+ method: "PUT",
620
+ headers: {
621
+ "Content-Type": "application/json",
622
+ Authorization: `Bearer ${jwt}`
623
+ },
624
+ body: JSON.stringify({ email_change_token: emailChangeToken })
625
+ });
626
+ if (!emailChangeRes.ok) {
627
+ const errorBody = await emailChangeRes.json().catch(() => ({}));
628
+ throw new AuthError(
629
+ errorBody.msg || `Email change verification failed (${emailChangeRes.status})`,
630
+ emailChangeRes.status
631
+ );
632
+ }
633
+ const emailChangeData = await emailChangeRes.json();
634
+ const user = toUser(emailChangeData);
635
+ clearHash();
636
+ emitAuthEvent(AUTH_EVENTS.USER_UPDATED, user);
637
+ return { type: "email_change", user };
638
+ };
631
639
  var clearHash = () => {
632
640
  history.replaceState(null, "", window.location.pathname + window.location.search);
633
641
  };
@@ -653,12 +661,12 @@ var hydrateSession = async () => {
653
661
  persistSession
654
662
  );
655
663
  const user = toUser(gotrueUser);
656
- emitAuthEvent("login", user);
664
+ emitAuthEvent(AUTH_EVENTS.LOGIN, user);
657
665
  return user;
658
666
  };
659
667
 
660
668
  // src/account.ts
661
- var ensureCurrentUser = async () => {
669
+ var resolveCurrentUser = async () => {
662
670
  const client = getClient();
663
671
  let currentUser = client.currentUser();
664
672
  if (!currentUser && isBrowser()) {
@@ -685,7 +693,7 @@ var recoverPassword = async (token, newPassword) => {
685
693
  const gotrueUser = await client.recover(token, persistSession);
686
694
  const updatedUser = await gotrueUser.update({ password: newPassword });
687
695
  const user = toUser(updatedUser);
688
- emitAuthEvent("login", user);
696
+ emitAuthEvent(AUTH_EVENTS.LOGIN, user);
689
697
  return user;
690
698
  } catch (error) {
691
699
  throw new AuthError(error.message, void 0, { cause: error });
@@ -696,7 +704,7 @@ var confirmEmail = async (token) => {
696
704
  try {
697
705
  const gotrueUser = await client.confirm(token, persistSession);
698
706
  const user = toUser(gotrueUser);
699
- emitAuthEvent("login", user);
707
+ emitAuthEvent(AUTH_EVENTS.LOGIN, user);
700
708
  return user;
701
709
  } catch (error) {
702
710
  throw new AuthError(error.message, void 0, { cause: error });
@@ -707,7 +715,7 @@ var acceptInvite = async (token, password) => {
707
715
  try {
708
716
  const gotrueUser = await client.acceptInvite(token, password, persistSession);
709
717
  const user = toUser(gotrueUser);
710
- emitAuthEvent("login", user);
718
+ emitAuthEvent(AUTH_EVENTS.LOGIN, user);
711
719
  return user;
712
720
  } catch (error) {
713
721
  throw new AuthError(error.message, void 0, { cause: error });
@@ -715,7 +723,7 @@ var acceptInvite = async (token, password) => {
715
723
  };
716
724
  var verifyEmailChange = async (token) => {
717
725
  if (!isBrowser()) throw new AuthError("verifyEmailChange() is only available in the browser");
718
- const currentUser = await ensureCurrentUser();
726
+ const currentUser = await resolveCurrentUser();
719
727
  const jwt = await currentUser.jwt();
720
728
  const identityUrl = `${window.location.origin}${IDENTITY_PATH}`;
721
729
  try {
@@ -729,14 +737,11 @@ var verifyEmailChange = async (token) => {
729
737
  });
730
738
  if (!res.ok) {
731
739
  const errorBody = await res.json().catch(() => ({}));
732
- throw new AuthError(
733
- errorBody.msg || `Email change verification failed (${res.status})`,
734
- res.status
735
- );
740
+ throw new AuthError(errorBody.msg || `Email change verification failed (${res.status})`, res.status);
736
741
  }
737
742
  const userData = await res.json();
738
743
  const user = toUser(userData);
739
- emitAuthEvent("user_updated", user);
744
+ emitAuthEvent(AUTH_EVENTS.USER_UPDATED, user);
740
745
  return user;
741
746
  } catch (error) {
742
747
  if (error instanceof AuthError) throw error;
@@ -744,11 +749,11 @@ var verifyEmailChange = async (token) => {
744
749
  }
745
750
  };
746
751
  var updateUser = async (updates) => {
747
- const currentUser = await ensureCurrentUser();
752
+ const currentUser = await resolveCurrentUser();
748
753
  try {
749
754
  const updatedUser = await currentUser.update(updates);
750
755
  const user = toUser(updatedUser);
751
- emitAuthEvent("user_updated", user);
756
+ emitAuthEvent(AUTH_EVENTS.USER_UPDATED, user);
752
757
  return user;
753
758
  } catch (error) {
754
759
  throw new AuthError(error.message, void 0, { cause: error });
@@ -756,6 +761,7 @@ var updateUser = async (updates) => {
756
761
  };
757
762
  // Annotate the CommonJS export names for ESM import in node:
758
763
  0 && (module.exports = {
764
+ AUTH_EVENTS,
759
765
  AuthError,
760
766
  MissingIdentityError,
761
767
  acceptInvite,