@netlify/identity 0.3.0-alpha.4 → 0.3.0-alpha.5

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
@@ -54,8 +54,8 @@ import { login, getUser } from '@netlify/identity'
54
54
  const user = await login('jane@example.com', 'password123')
55
55
  console.log(`Hello, ${user.name}`)
56
56
 
57
- // Later, check auth state synchronously
58
- const currentUser = getUser()
57
+ // Later, check auth state
58
+ const currentUser = await getUser()
59
59
  ```
60
60
 
61
61
  ### Protect a Netlify Function
@@ -65,7 +65,7 @@ import { getUser } from '@netlify/identity'
65
65
  import type { Context } from '@netlify/functions'
66
66
 
67
67
  export default async (req: Request, context: Context) => {
68
- const user = getUser()
68
+ const user = await getUser()
69
69
  if (!user) return new Response('Unauthorized', { status: 401 })
70
70
  return Response.json({ id: user.id, email: user.email })
71
71
  }
@@ -78,7 +78,7 @@ import { getUser } from '@netlify/identity'
78
78
  import type { Context } from '@netlify/edge-functions'
79
79
 
80
80
  export default async (req: Request, context: Context) => {
81
- const user = getUser()
81
+ const user = await getUser()
82
82
  if (!user) return new Response('Unauthorized', { status: 401 })
83
83
  return Response.json({ id: user.id, email: user.email })
84
84
  }
@@ -91,20 +91,20 @@ export default async (req: Request, context: Context) => {
91
91
  #### `getUser`
92
92
 
93
93
  ```ts
94
- getUser(): User | null
94
+ getUser(): Promise<User | null>
95
95
  ```
96
96
 
97
- Returns the current authenticated user, or `null` if not logged in. Synchronous. Never throws.
97
+ Returns the current authenticated user, or `null` if not logged in. Always returns a full `User` object with all available fields (email, roles, timestamps, metadata) regardless of whether the call happens in the browser or on the server. Never throws.
98
98
 
99
99
  > **Next.js note:** Calling `getUser()` in a Server Component opts the page into [dynamic rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-rendering) because it reads cookies. This is expected and correct for authenticated pages. Next.js handles the internal dynamic rendering signal automatically.
100
100
 
101
101
  #### `isAuthenticated`
102
102
 
103
103
  ```ts
104
- isAuthenticated(): boolean
104
+ isAuthenticated(): Promise<boolean>
105
105
  ```
106
106
 
107
- Returns `true` if a user is currently authenticated. Equivalent to `getUser() !== null`. Never throws.
107
+ Returns `true` if a user is currently authenticated. Equivalent to `(await getUser()) !== null`. Never throws.
108
108
 
109
109
  #### `getIdentityConfig`
110
110
 
@@ -200,7 +200,7 @@ hydrateSession(): Promise<User | null>
200
200
 
201
201
  Bootstraps the browser-side gotrue-js 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 gotrue-js has no browser session yet. `getUser()` works immediately (it decodes the cookie), but account operations like `updateUser()` or `verifyEmailChange()` require a live gotrue-js session. Call `hydrateSession()` once on page load to bridge this gap.
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 gotrue-js has no browser session yet. `getUser()` calls `hydrateSession()` automatically, but account operations like `updateUser()` or `verifyEmailChange()` require a live gotrue-js session. Call `hydrateSession()` explicitly if you need the session ready before calling those operations.
204
204
 
205
205
  If a gotrue-js session already exists (e.g., from a browser-side login), this is a no-op and returns the existing user.
206
206
 
@@ -373,6 +373,7 @@ interface User {
373
373
  provider?: AuthProvider
374
374
  name?: string
375
375
  pictureUrl?: string
376
+ roles?: string[]
376
377
  metadata?: Record<string, unknown>
377
378
  rawGoTrueData?: Record<string, unknown>
378
379
  }
@@ -611,8 +612,8 @@ export default function LoginPage() {
611
612
  import { getUser } from '@netlify/identity'
612
613
  import { redirect } from 'next/navigation'
613
614
 
614
- export default function Dashboard() {
615
- const user = getUser()
615
+ export default async function Dashboard() {
616
+ const user = await getUser()
616
617
  if (!user) redirect('/login')
617
618
 
618
619
  return <h1>Hello, {user.email}</h1>
@@ -651,7 +652,7 @@ import { getUser } from '@netlify/identity'
651
652
  import { redirect } from '@remix-run/node'
652
653
 
653
654
  export async function loader() {
654
- const user = getUser()
655
+ const user = await getUser()
655
656
  if (!user) return redirect('/login')
656
657
  return { user }
657
658
  }
@@ -669,7 +670,7 @@ import { createServerFn } from '@tanstack/react-start'
669
670
  import { getUser } from '@netlify/identity'
670
671
 
671
672
  export const getServerUser = createServerFn({ method: 'GET' }).handler(async () => {
672
- const user = getUser()
673
+ const user = await getUser()
673
674
  return user ?? null
674
675
  })
675
676
  ```
@@ -755,7 +756,7 @@ export const POST: APIRoute = async ({ request }) => {
755
756
  // src/pages/dashboard.astro
756
757
  import { getUser } from '@netlify/identity'
757
758
 
758
- const user = getUser()
759
+ const user = await getUser()
759
760
  if (!user) return Astro.redirect('/login')
760
761
  ---
761
762
  <h1>Hello, {user.email}</h1>
@@ -797,8 +798,8 @@ if (!user) return Astro.redirect('/login')
797
798
  import { getUser } from '@netlify/identity'
798
799
  import { redirect } from '@sveltejs/kit'
799
800
 
800
- export function load() {
801
- const user = getUser()
801
+ export async function load() {
802
+ const user = await getUser()
802
803
  if (!user) redirect(302, '/login')
803
804
  return { user }
804
805
  }
@@ -873,9 +874,10 @@ import { getUser, onAuthChange } from '@netlify/identity'
873
874
  import type { User } from '@netlify/identity'
874
875
 
875
876
  export function useAuth() {
876
- const [user, setUser] = useState<User | null>(getUser())
877
+ const [user, setUser] = useState<User | null>(null)
877
878
 
878
879
  useEffect(() => {
880
+ getUser().then(setUser)
879
881
  return onAuthChange((_event, user) => setUser(user))
880
882
  }, [])
881
883
 
package/dist/index.cjs CHANGED
@@ -222,143 +222,6 @@ var triggerNextjsDynamic = () => {
222
222
  }
223
223
  };
224
224
 
225
- // src/user.ts
226
- var toAuthProvider = (value) => typeof value === "string" && AUTH_PROVIDERS.includes(value) ? value : void 0;
227
- var toUser = (userData) => {
228
- const userMeta = userData.user_metadata ?? {};
229
- const appMeta = userData.app_metadata ?? {};
230
- const name = userMeta.full_name || userMeta.name;
231
- const pictureUrl = userMeta.avatar_url;
232
- return {
233
- id: userData.id,
234
- email: userData.email,
235
- emailVerified: !!userData.confirmed_at,
236
- createdAt: userData.created_at,
237
- updatedAt: userData.updated_at,
238
- provider: toAuthProvider(appMeta.provider),
239
- name: typeof name === "string" ? name : void 0,
240
- pictureUrl: typeof pictureUrl === "string" ? pictureUrl : void 0,
241
- metadata: userMeta,
242
- rawGoTrueData: { ...userData }
243
- };
244
- };
245
- var claimsToUser = (claims) => {
246
- const appMeta = claims.app_metadata ?? {};
247
- const userMeta = claims.user_metadata ?? {};
248
- const name = userMeta.full_name || userMeta.name;
249
- return {
250
- id: claims.sub ?? "",
251
- email: claims.email,
252
- provider: toAuthProvider(appMeta.provider),
253
- name: typeof name === "string" ? name : void 0,
254
- metadata: userMeta
255
- };
256
- };
257
- var hydrating = false;
258
- var backgroundHydrate = (accessToken) => {
259
- if (hydrating) return;
260
- hydrating = true;
261
- const refreshToken = getCookie(NF_REFRESH_COOKIE) ?? "";
262
- const decoded = decodeJwtPayload(accessToken);
263
- const expiresAt = decoded?.exp ?? Math.floor(Date.now() / 1e3) + 3600;
264
- const expiresIn = Math.max(0, expiresAt - Math.floor(Date.now() / 1e3));
265
- setTimeout(() => {
266
- try {
267
- const client = getClient();
268
- client.createUser(
269
- {
270
- access_token: accessToken,
271
- token_type: "bearer",
272
- expires_in: expiresIn,
273
- expires_at: expiresAt,
274
- refresh_token: refreshToken
275
- },
276
- true
277
- ).catch(() => {
278
- }).finally(() => {
279
- hydrating = false;
280
- });
281
- } catch {
282
- hydrating = false;
283
- }
284
- }, 0);
285
- };
286
- var decodeJwtPayload = (token) => {
287
- try {
288
- const parts = token.split(".");
289
- if (parts.length !== 3) return null;
290
- const payload = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"));
291
- return JSON.parse(payload);
292
- } catch {
293
- return null;
294
- }
295
- };
296
- var getUser = () => {
297
- if (isBrowser()) {
298
- const client = getGoTrueClient();
299
- const currentUser = client?.currentUser() ?? null;
300
- if (currentUser) {
301
- const jwt2 = getCookie(NF_JWT_COOKIE);
302
- if (!jwt2) {
303
- try {
304
- currentUser.clearSession();
305
- } catch {
306
- }
307
- return null;
308
- }
309
- return toUser(currentUser);
310
- }
311
- const jwt = getCookie(NF_JWT_COOKIE);
312
- if (!jwt) return null;
313
- const claims = decodeJwtPayload(jwt);
314
- if (!claims) return null;
315
- backgroundHydrate(jwt);
316
- return claimsToUser(claims);
317
- }
318
- triggerNextjsDynamic();
319
- const identityContext = globalThis.netlifyIdentityContext;
320
- if (identityContext?.user) {
321
- return claimsToUser(identityContext.user);
322
- }
323
- const serverJwt = getServerCookie(NF_JWT_COOKIE);
324
- if (serverJwt) {
325
- const claims = decodeJwtPayload(serverJwt);
326
- if (claims) return claimsToUser(claims);
327
- }
328
- return null;
329
- };
330
- var isAuthenticated = () => getUser() !== null;
331
-
332
- // src/config.ts
333
- var getIdentityConfig = () => {
334
- if (isBrowser()) {
335
- return { url: `${window.location.origin}${IDENTITY_PATH}` };
336
- }
337
- return getIdentityContext();
338
- };
339
- var getSettings = async () => {
340
- const client = getClient();
341
- try {
342
- const raw = await client.settings();
343
- const external = raw.external ?? {};
344
- return {
345
- autoconfirm: raw.autoconfirm,
346
- disableSignup: raw.disable_signup,
347
- providers: {
348
- google: external.google ?? false,
349
- github: external.github ?? false,
350
- gitlab: external.gitlab ?? false,
351
- bitbucket: external.bitbucket ?? false,
352
- facebook: external.facebook ?? false,
353
- email: external.email ?? false,
354
- saml: external.saml ?? false
355
- }
356
- };
357
- } catch (err) {
358
- throw new AuthError(err instanceof Error ? err.message : "Failed to fetch identity settings", 502, { cause: err });
359
- }
360
- };
361
-
362
225
  // src/events.ts
363
226
  var AUTH_EVENTS = {
364
227
  LOGIN: "login",
@@ -681,6 +544,151 @@ var hydrateSession = async () => {
681
544
  return user;
682
545
  };
683
546
 
547
+ // src/user.ts
548
+ var toAuthProvider = (value) => typeof value === "string" && AUTH_PROVIDERS.includes(value) ? value : void 0;
549
+ var toRoles = (appMeta) => {
550
+ const roles = appMeta.roles;
551
+ if (Array.isArray(roles) && roles.every((r) => typeof r === "string")) {
552
+ return roles;
553
+ }
554
+ return void 0;
555
+ };
556
+ var toUser = (userData) => {
557
+ const userMeta = userData.user_metadata ?? {};
558
+ const appMeta = userData.app_metadata ?? {};
559
+ const name = userMeta.full_name || userMeta.name;
560
+ const pictureUrl = userMeta.avatar_url;
561
+ return {
562
+ id: userData.id,
563
+ email: userData.email,
564
+ emailVerified: !!userData.confirmed_at,
565
+ createdAt: userData.created_at,
566
+ updatedAt: userData.updated_at,
567
+ provider: toAuthProvider(appMeta.provider),
568
+ name: typeof name === "string" ? name : void 0,
569
+ pictureUrl: typeof pictureUrl === "string" ? pictureUrl : void 0,
570
+ roles: toRoles(appMeta),
571
+ metadata: userMeta,
572
+ rawGoTrueData: { ...userData }
573
+ };
574
+ };
575
+ var claimsToUser = (claims) => {
576
+ const appMeta = claims.app_metadata ?? {};
577
+ const userMeta = claims.user_metadata ?? {};
578
+ const name = userMeta.full_name || userMeta.name;
579
+ const pictureUrl = userMeta.avatar_url;
580
+ return {
581
+ id: claims.sub ?? "",
582
+ email: claims.email,
583
+ provider: toAuthProvider(appMeta.provider),
584
+ name: typeof name === "string" ? name : void 0,
585
+ pictureUrl: typeof pictureUrl === "string" ? pictureUrl : void 0,
586
+ roles: toRoles(appMeta),
587
+ metadata: userMeta
588
+ };
589
+ };
590
+ var decodeJwtPayload = (token) => {
591
+ try {
592
+ const parts = token.split(".");
593
+ if (parts.length !== 3) return null;
594
+ const payload = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"));
595
+ return JSON.parse(payload);
596
+ } catch {
597
+ return null;
598
+ }
599
+ };
600
+ var fetchFullUser = async (identityUrl, jwt) => {
601
+ try {
602
+ const res = await fetch(`${identityUrl}/user`, {
603
+ headers: { Authorization: `Bearer ${jwt}` }
604
+ });
605
+ if (!res.ok) return null;
606
+ const userData = await res.json();
607
+ return toUser(userData);
608
+ } catch {
609
+ return null;
610
+ }
611
+ };
612
+ var resolveIdentityUrl = () => {
613
+ const identityContext = getIdentityContext();
614
+ if (identityContext?.url) return identityContext.url;
615
+ if (globalThis.Netlify?.context?.url) {
616
+ return new URL(IDENTITY_PATH, globalThis.Netlify.context.url).href;
617
+ }
618
+ const siteUrl = typeof process !== "undefined" ? process.env?.URL : void 0;
619
+ if (siteUrl) {
620
+ return new URL(IDENTITY_PATH, siteUrl).href;
621
+ }
622
+ return null;
623
+ };
624
+ var getUser = async () => {
625
+ if (isBrowser()) {
626
+ const client = getGoTrueClient();
627
+ const currentUser = client?.currentUser() ?? null;
628
+ if (currentUser) {
629
+ const jwt2 = getCookie(NF_JWT_COOKIE);
630
+ if (!jwt2) {
631
+ try {
632
+ currentUser.clearSession();
633
+ } catch {
634
+ }
635
+ return null;
636
+ }
637
+ return toUser(currentUser);
638
+ }
639
+ const jwt = getCookie(NF_JWT_COOKIE);
640
+ if (!jwt) return null;
641
+ const claims2 = decodeJwtPayload(jwt);
642
+ if (!claims2) return null;
643
+ const hydrated = await hydrateSession();
644
+ if (hydrated) return hydrated;
645
+ return claimsToUser(claims2);
646
+ }
647
+ triggerNextjsDynamic();
648
+ const identityContext = globalThis.netlifyIdentityContext;
649
+ const serverJwt = identityContext?.token || getServerCookie(NF_JWT_COOKIE);
650
+ if (serverJwt) {
651
+ const identityUrl = resolveIdentityUrl();
652
+ if (identityUrl) {
653
+ const fullUser = await fetchFullUser(identityUrl, serverJwt);
654
+ if (fullUser) return fullUser;
655
+ }
656
+ }
657
+ const claims = identityContext?.user ?? (serverJwt ? decodeJwtPayload(serverJwt) : null);
658
+ return claims ? claimsToUser(claims) : null;
659
+ };
660
+ var isAuthenticated = async () => await getUser() !== null;
661
+
662
+ // src/config.ts
663
+ var getIdentityConfig = () => {
664
+ if (isBrowser()) {
665
+ return { url: `${window.location.origin}${IDENTITY_PATH}` };
666
+ }
667
+ return getIdentityContext();
668
+ };
669
+ var getSettings = async () => {
670
+ const client = getClient();
671
+ try {
672
+ const raw = await client.settings();
673
+ const external = raw.external ?? {};
674
+ return {
675
+ autoconfirm: raw.autoconfirm,
676
+ disableSignup: raw.disable_signup,
677
+ providers: {
678
+ google: external.google ?? false,
679
+ github: external.github ?? false,
680
+ gitlab: external.gitlab ?? false,
681
+ bitbucket: external.bitbucket ?? false,
682
+ facebook: external.facebook ?? false,
683
+ email: external.email ?? false,
684
+ saml: external.saml ?? false
685
+ }
686
+ };
687
+ } catch (err) {
688
+ throw new AuthError(err instanceof Error ? err.message : "Failed to fetch identity settings", 502, { cause: err });
689
+ }
690
+ };
691
+
684
692
  // src/account.ts
685
693
  var resolveCurrentUser = async () => {
686
694
  const client = getClient();