Repository: avelinapp/avelin
Pull request #269: fix(auth/jwt): resolved jwt issues
Author: login: kianbazza
Created: 2025-05-08T02:47:59Z | Merged: 2025-05-08T03:02:26Z

chore(deps): bump better-auth to v1.2.7

feat(auth): set jwt after login callback endpoint

fix(auth): parse jwt token exp field as unix timestamp

fix(auth): check session jwt is not 'undefined' string

feat(auth): create refreshJwt func

refactor(zero): use refreshJwt func

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added a method to refresh JWT tokens, making it accessible through authentication context and providers.
  - Introduced a function to clear the Zero client instance.

- **Bug Fixes**
  - Improved handling of JWT cookie values to prevent treating the string 'undefined' as a valid token.
  - Corrected expiration handling for JWT cookies by converting timestamps to proper date objects.

- **Chores**
  - Updated JWT configuration and middleware to enhance token management and cookie settings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/web/lib/zero.ts b/apps/web/lib/zero.ts
index 7978c4f..88a7b53 100644
--- a/apps/web/lib/zero.ts
+++ b/apps/web/lib/zero.ts
@@ -2,7 +2,6 @@ import { type AuthData, type ZeroSchema, schema } from '@avelin/zero'
 import { Zero } from '@rocicorp/zero'
 import { createUseZero } from '@rocicorp/zero/react'
 import type { Metadata } from 'next'
-import { authClient } from './auth'
 import { env } from './env'
 
 export const useZero = createUseZero<ZeroSchema>()
@@ -16,7 +15,12 @@ export const metadata: Metadata = {
 export function getZeroClient({
   jwt,
   payload,
-}: { jwt?: string; payload?: AuthData }) {
+  refreshJwt,
+}: {
+  jwt?: string
+  payload?: AuthData
+  refreshJwt: () => Promise<string>
+}) {
   if (!client) {
     console.log(
       typeof window !== 'undefined' ? '[CLIENT]' : '[SERVER]',
@@ -33,32 +37,14 @@ export function getZeroClient({
       server: env.NEXT_PUBLIC_ZERO_URL,
       kvStore: 'idb',
       auth: async (error?: 'invalid-token') => {
+        // Refresh JWT if missing or invalid
         if (!jwt || error === 'invalid-token') {
-          console.log('invalid token, refreshing...')
-          const { data, error } = await authClient.getSession()
-
-          if (error) {
-            throw error
-          }
-
-          console.log('[Zero] session data:', data)
-
-          const res = await fetch(`${env.NEXT_PUBLIC_API_URL}/auth/token`, {
-            method: 'GET',
-            headers: {
-              Authorization: `Bearer ${data.session.token}`,
-            },
-          })
-
-          if (!res.ok) {
-            throw new Error('Failed to refresh token')
-          }
-
-          const { jwt } = (await res.json()) as { jwt: string }
-
-          return jwt
+          console.log('[Zero] Auth - invalid token, refreshing...')
+          return await refreshJwt()
         }
 
+        console.log('[Zero] Auth - using existing token:', jwt)
+
         return jwt
       },
     })
@@ -72,4 +58,9 @@ export function getZeroClient({
   return client
 }
 
+export function clearZeroClient() {
+  client?.close()
+  client = undefined
+}
+
 export const now = () => Date.now()
diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts
index 5c564b7..86a72da 100644
--- a/apps/web/middleware.ts
+++ b/apps/web/middleware.ts
@@ -28,7 +28,8 @@ export async function middleware(request: NextRequest) {
   if (sessionToken?.includes('.')) {
     sessionToken = sessionToken.split('.')[0]
   }
-  const sessionJwt = request.cookies.get('avelin.session_jwt')?.value
+  let sessionJwt = request.cookies.get('avelin.session_jwt')?.value
+  if (sessionJwt === 'undefined') sessionJwt = undefined
 
   console.log('Session token:', sessionToken)
   console.log('Session JWT:', sessionJwt)
@@ -66,8 +67,8 @@ export async function middleware(request: NextRequest) {
       return NextResponse.redirect(new URL('/login', request.url))
     }
 
-    // This value is in seconds
     const jwtExpiration = decodeJwt(jwt.token).exp ?? 0
+    const jwtExpirationDate = new Date(jwtExpiration * 1000)
 
     response.cookies.set('avelin.session_jwt', jwt.token, {
       path: '/',
@@ -75,7 +76,7 @@ export async function middleware(request: NextRequest) {
       secure: env.NODE_ENV === 'production',
       sameSite: 'lax',
       domain: `.${env.NEXT_PUBLIC_BASE_DOMAIN}`,
-      expires: jwtExpiration * 1000, // Convert to milliseconds
+      expires: jwtExpirationDate,
     })
 
     console.timeEnd('middleware')
@@ -134,16 +135,16 @@ export async function middleware(request: NextRequest) {
 
     console.log('JWT payload:', decodeJwt(jwt.token))
 
-    // This value is in seconds
     const jwtExpiration = decodeJwt(jwt.token).exp ?? 0
+    const jwtExpirationDate = new Date(jwtExpiration * 1000)
 
     response.cookies.set('avelin.session_jwt', jwt.token, {
       path: '/',
       httpOnly: false,
-      sameSite: 'lax',
       secure: env.NODE_ENV === 'production',
+      sameSite: 'lax',
       domain: `.${env.NEXT_PUBLIC_BASE_DOMAIN}`,
-      expires: jwtExpiration * 1000, // Convert to milliseconds
+      expires: jwtExpirationDate,
     })
   }
 
diff --git a/apps/web/providers/auth-provider.tsx b/apps/web/providers/auth-provider.tsx
index 94c137a..1640155 100644
--- a/apps/web/providers/auth-provider.tsx
+++ b/apps/web/providers/auth-provider.tsx
@@ -1,15 +1,10 @@
 'use client'
 
 import { authClient } from '@/lib/auth'
+import { env } from '@/lib/env'
+import Cookies from 'js-cookie'
 import { useRouter } from 'next/navigation'
-const { useSession } = authClient
-import {
-  createContext,
-  useCallback,
-  useContext,
-  useEffect,
-  useState,
-} from 'react'
+import { createContext, useCallback, useContext, useState } from 'react'
 
 type User = typeof authClient.$Infer.Session.user
 type Session = typeof authClient.$Infer.Session.session
@@ -40,6 +35,7 @@ export type AuthState =
 
 export type AuthActions = {
   signOut: () => Promise<void>
+  refreshJwt: () => Promise<string>
 }
 
 export type AuthStore = AuthState & AuthActions
@@ -48,6 +44,7 @@ const AuthContext = createContext<AuthStore>({
   isPending: true,
   isAuthenticated: false,
   signOut: async () => {},
+  refreshJwt: async () => '',
 })
 
 type AuthData = {
@@ -78,7 +75,43 @@ export default function AuthProvider({
     await authClient.signOut()
     setData(undefined)
     router.push('/login')
-  }, [])
+  }, [router])
+
+  const refreshJwt = useCallback(async () => {
+    const { data, error } = await authClient.getSession()
+
+    console.log('[AuthProvider] refreshJwt', data, error)
+
+    if (!data || error) {
+      return signOut()
+    }
+
+    const res = await fetch(`${env.NEXT_PUBLIC_API_URL}/auth/token`, {
+      method: 'GET',
+      headers: {
+        Authorization: `Bearer ${data.session.token}`,
+      },
+    })
+
+    if (!res.ok) {
+      return signOut()
+    }
+
+    const { jwt } = (await res.json()) as { jwt: string }
+
+    Cookies.set('avelin.session_jwt', jwt, {
+      path: '/',
+      httpOnly: false,
+      secure: process.env.NODE_ENV === 'production',
+      sameSite: 'lax',
+      domain: `.${env.NEXT_PUBLIC_BASE_DOMAIN}`,
+      expires: 1,
+    })
+
+    console.log('[AuthProvider] refreshJwt => JWT refreshed:', jwt)
+
+    return jwt
+  }, [signOut])
 
   const value = {
     user: data?.user,
@@ -87,6 +120,7 @@ export default function AuthProvider({
     isAuthenticated,
     isAnonymous,
     signOut,
+    refreshJwt,
   }
 
   // @ts-expect-error
diff --git a/apps/web/providers/zero-provider.tsx b/apps/web/providers/zero-provider.tsx
index 56e77a8..584b350 100644
--- a/apps/web/providers/zero-provider.tsx
+++ b/apps/web/providers/zero-provider.tsx
@@ -5,6 +5,7 @@ import type { AuthData } from '@avelin/zero'
 import { ZeroProvider as ZeroProviderPrimitive } from '@rocicorp/zero/react'
 import { decodeJwt } from 'jose/jwt/decode'
 import Cookies from 'js-cookie'
+import { useAuth } from './auth-provider'
 import ViewProvider from './view-provider'
 
 export default function ZeroProvider({
@@ -12,12 +13,11 @@ export default function ZeroProvider({
 }: {
   children: React.ReactNode
 }) {
+  const { refreshJwt } = useAuth()
   const jwt = Cookies.get('avelin.session_jwt')
   const payload = jwt ? (decodeJwt(jwt) as AuthData) : undefined
 
-  console.log('[ZeroProvider] JWT payload:', payload)
-
-  const z = getZeroClient({ jwt, payload })
+  const z = getZeroClient({ jwt, payload, refreshJwt })
 
   z.query.rooms
     .where('deletedAt', 'IS', null)
diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts
index 5986c3b..2a8bff0 100644
--- a/packages/auth/src/index.ts
+++ b/packages/auth/src/index.ts
@@ -1,7 +1,15 @@
 import { db, schema } from '@avelin/database'
 import { betterAuth } from 'better-auth'
 import { drizzleAdapter } from 'better-auth/adapters/drizzle'
-import { anonymous, bearer, jwt, openAPI } from 'better-auth/plugins'
+import {
+  type JwtOptions,
+  anonymous,
+  bearer,
+  createAuthMiddleware,
+  getJwtToken,
+  jwt,
+  openAPI,
+} from 'better-auth/plugins'
 
 const APP_URL = (process.env.APP_URL ||
   process.env.NEXT_PUBLIC_APP_URL) as string
@@ -13,6 +21,12 @@ const BASE_DOMAIN = (process.env.BASE_DOMAIN ||
 export type User = typeof auth.$Infer.Session.user
 export type Session = typeof auth.$Infer.Session.session
 
+const jwtOptions = {
+  jwt: {
+    expirationTime: '1d',
+  },
+} as const satisfies JwtOptions
+
 export const auth = betterAuth({
   trustedOrigins: [APP_URL],
   database: drizzleAdapter(db, {
@@ -37,11 +51,7 @@ export const auth = betterAuth({
     anonymous({
       emailDomainName: 'anon.avelin.app',
     }),
-    jwt({
-      jwt: {
-        expirationTime: '1d',
-      },
-    }),
+    jwt(jwtOptions),
     openAPI(),
   ],
   session: {
@@ -64,6 +74,23 @@ export const auth = betterAuth({
       domain: `.${BASE_DOMAIN}`,
     },
   },
+  hooks: {
+    after: createAuthMiddleware(async (ctx) => {
+      if (ctx.path.startsWith('/callback')) {
+        ctx.context.session = ctx.context.newSession
+        const jwt = await getJwtToken(ctx, jwtOptions)
+        console.log('[Auth] JWT:', jwt)
+        ctx.setCookie('avelin.session_jwt', jwt, {
+          path: '/',
+          httpOnly: false,
+          secure: process.env.NODE_ENV === 'production',
+          sameSite: 'lax',
+          domain: `.${BASE_DOMAIN}`,
+          maxAge: 60 * 60 * 24, // 24h
+        })
+      }
+    }),
+  },
 })
 
 export const authCookies = (await auth.$context).authCookies


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>apps/web/middleware.ts (1)</summary><blockquote>

`3-3`: **Unused import detected**

The `addSeconds` import from 'date-fns' is not being used in the code.

```diff
-import { addSeconds } from 'date-fns'
```

</blockquote></details>
<details>
<summary>apps/web/providers/auth-provider.tsx (1)</summary><blockquote>

`80-85`: **Dependency array: prefer `router` over `router.push`**  
`router.push` is a bound method whose identity is stable, but Next.js doesn’t guarantee it never changes.  Depending on the whole router object is simpler and avoids an ESLint warning in stricter configs:

```diff
-  }, [router.push])
+  }, [router])
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between fd94d17cf17b78e5b19b45c0f59914658ca2f51b and b01c1532169d1fb9739bda19001a54586c61ea80.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `apps/web/lib/zero.ts` (3 hunks)
* `apps/web/middleware.ts` (4 hunks)
* `apps/web/providers/auth-provider.tsx` (5 hunks)
* `apps/web/providers/zero-provider.tsx` (1 hunks)
* `packages/auth/src/index.ts` (4 hunks)
* `pnpm-workspace.yaml` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>apps/web/middleware.ts (1)</summary><blockquote>

<details>
<summary>apps/web/lib/env.ts (1)</summary>

* `env` (4-106)

</details>

</blockquote></details>
<details>
<summary>apps/web/lib/zero.ts (2)</summary><blockquote>

<details>
<summary>packages/zero/src/schema.ts (1)</summary>

* `AuthData` (87-94)

</details>
<details>
<summary>apps/api/src/client.ts (1)</summary>

* `client` (5-10)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (9)</summary><blockquote>

<details>
<summary>pnpm-workspace.yaml (1)</summary>

`14-14`: **Version update aligned with PR objectives**

The version bump for `better-auth` from v1.2.3 to v1.2.7 corresponds with the PR objectives to update this dependency with new features and fixes for JWT handling.

</details>
<details>
<summary>apps/web/providers/zero-provider.tsx (1)</summary>

`8-8`: **Good integration of JWT refresh functionality**

The changes properly integrate the new JWT refresh mechanism by:
1. Importing the `useAuth` hook
2. Extracting the `refreshJwt` function
3. Passing it to `getZeroClient` alongside the existing JWT parameters

This cleanly separates authentication concerns and ensures the Zero client can refresh tokens as needed.



Also applies to: 16-16, 20-20

</details>
<details>
<summary>apps/web/lib/zero.ts (3)</summary>

`18-23`: **Well-designed function signature update**

The updated signature properly captures the new requirement for a JWT refresh callback function. The TypeScript types are well-defined, ensuring type safety when implementing this interface.

---

`40-47`: **Improved authentication flow with better logging**

The authentication flow has been improved by:
1. Adding clear comments
2. Using the provided `refreshJwt` callback when tokens are invalid
3. Adding helpful debug logs that indicate when tokens are refreshed or reused

This approach follows good dependency injection principles by delegating token refresh responsibility to the caller.

---

`61-64`: **Good addition of client cleanup function**

The new `clearZeroClient` function properly manages the Zero client lifecycle by:
1. Closing the client connection
2. Resetting the client reference

This helps prevent memory leaks and ensures proper cleanup when needed.

</details>
<details>
<summary>apps/web/middleware.ts (3)</summary>

`32-33`: **Good protection against invalid JWT values**

The change from `const` to `let` for the sessionJwt variable and the check to convert the literal string 'undefined' to an actual `undefined` value prevents handling 'undefined' as a valid JWT token.

---

`72-72`: **Improved JWT expiration handling**

The code now correctly converts the JWT expiration timestamp (in seconds) to a JavaScript Date object by multiplying by 1000. This ensures the cookie expiration is set properly using a Date object rather than a numeric timestamp.



Also applies to: 80-80

---

`140-140`: **Consistent JWT expiration handling**

This change correctly applies the same JWT expiration handling pattern as earlier in the file, ensuring consistency in how expiration dates are processed throughout the middleware.



Also applies to: 148-148

</details>
<details>
<summary>packages/auth/src/index.ts (1)</summary>

`24-29`: **JWT options object looks solid**  
Strong ✅ for extracting the JWT configuration into a typed `jwtOptions` constant.  This improves re-usability and keeps the inline plugin call cleaner.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-08T02:52:06Z

Review comments:
diff_hunk: @@ -64,6 +74,23 @@ export const auth = betterAuth({
       domain: `.${BASE_DOMAIN}`,
     },
   },
+  hooks: {
+    after: createAuthMiddleware(async (ctx) => {
+      if (ctx.path.startsWith('/callback')) {
+        ctx.context.session = ctx.context.newSession
+        const jwt = await getJwtToken(ctx, jwtOptions)
+        console.log('[Auth] JWT:', jwt)
+        ctx.setCookie('avelin.session_jwt', jwt, {
+          path: '/',
+          httpOnly: false,
+          secure: process.env.NODE_ENV === 'production',
+          sameSite: 'lax',
+          domain: `.${BASE_DOMAIN}`,
+          maxAge: 60 * 60 * 24, // 24h
+        })
+      }
+    }),
path: packages/auth/src/index.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**Avoid leaking sensitive JWTs & tighten cookie security**  
`console.log('[Auth] JWT:', jwt)` and the **non-HTTP-only** cookie both expose the token to the browser console and any XSS payload.  Unless you have a very specific reason to let client-side scripts access the JWT, consider:

```diff
-        const jwt = await getJwtToken(ctx, jwtOptions)
-        console.log('[Auth] JWT:', jwt)
+        // Generate a short-lived JWT for the session
+        const jwt = await getJwtToken(ctx, jwtOptions)

@@
-          httpOnly: false,
+          httpOnly: true,
```

If the token must be readable client-side, at least gate the `console.log` behind `process.env.NODE_ENV !== 'production'` to prevent accidental disclosure in prod.  
 


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    after: createAuthMiddleware(async (ctx) => {
      if (ctx.path.startsWith('/callback')) {
        ctx.context.session = ctx.context.newSession
        // Generate a short-lived JWT for the session
        const jwt = await getJwtToken(ctx, jwtOptions)
        ctx.setCookie('avelin.session_jwt', jwt, {
          path: '/',
          httpOnly: true,
          secure: process.env.NODE_ENV === 'production',
          sameSite: 'lax',
          domain: `.${BASE_DOMAIN}`,
          maxAge: 60 * 60 * 24, // 24h
        })
      }
    }),
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T02:52:05Z
author_association: NONE
diff_hunk: @@ -40,6 +41,7 @@ export type AuthState =
 
 export type AuthActions = {
   signOut: () => Promise<void>
+  refreshJwt: () => Promise<string>
 }
path: apps/web/providers/auth-provider.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Return-type mismatch will break compilation**  
`refreshJwt` is declared as `() => Promise<string>`, but inside the implementation you sometimes `return signOut()` which resolves to `void`.  Either make `refreshJwt` always resolve to a string (e.g. return `''` after `signOut`) or widen the interface:

```diff
-export type AuthActions = {
-  signOut: () => Promise<void>
-  refreshJwt: () => Promise<string>
-}
+export type AuthActions = {
+  signOut: () => Promise<void>
+  // May return the new JWT string or `undefined` when the user is signed out.
+  refreshJwt: () => Promise<string | void>
+}
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T02:52:05Z
author_association: NONE
diff_hunk: @@ -78,7 +81,43 @@ export default function AuthProvider({
     await authClient.signOut()
     setData(undefined)
     router.push('/login')
-  }, [])
+  }, [router.push])
+
+  const refreshJwt = useCallback(async () => {
+    const { data, error } = await authClient.getSession()
+
+    console.log('[AuthProvider] refreshJwt', data, error)
+
+    if (!data || error) {
+      return signOut()
+    }
+
+    const res = await fetch(`${env.NEXT_PUBLIC_API_URL}/auth/token`, {
+      method: 'GET',
+      headers: {
+        Authorization: `Bearer ${data.session.token}`,
+      },
+    })
+
+    if (!res.ok) {
+      return signOut()
+    }
+
+    const { jwt } = (await res.json()) as { jwt: string }
+
+    Cookies.set('avelin.session_jwt', jwt, {
+      path: '/',
+      httpOnly: false,
+      secure: process.env.NODE_ENV === 'production',
+      sameSite: 'lax',
+      domain: `.${env.NEXT_PUBLIC_BASE_DOMAIN}`,
+      expires: 1,
+    })
+
+    console.log('[AuthProvider] refreshJwt => JWT refreshed:', jwt)
+
+    return jwt
+  }, [signOut])
path: apps/web/providers/auth-provider.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Harden `refreshJwt` – unhandled rejections, insecure cookie flag & noisy logs**  
1. `fetch` network failures aren’t caught; an exception will bubble up and break the caller.  
2. `httpOnly` cannot be set from client code and is silently ignored by `js-cookie`; leaving it here is misleading.  
3. Logging the refreshed token is useful in dev, dangerous in prod.

Suggested patch:

```diff
-  const refreshJwt = useCallback(async () => {
+  const refreshJwt = useCallback(async (): Promise<string | void> => {
     const { data, error } = await authClient.getSession()
@@
-    const res = await fetch(`${env.NEXT_PUBLIC_API_URL}/auth/token`, {
+    let res: Response
+    try {
+      res = await fetch(`${env.NEXT_PUBLIC_API_URL}/auth/token`, {
         method: 'GET',
         headers: {
           Authorization: `Bearer ${data.session.token}`,
         },
       })
+    } catch (e) {
+      console.error('[AuthProvider] refreshJwt fetch failed', e)
+      return signOut()
+    }
@@
-    Cookies.set('avelin.session_jwt', jwt, {
+    Cookies.set('avelin.session_jwt', jwt, {
       path: '/',
-      httpOnly: false,
       secure: process.env.NODE_ENV === 'production',
       sameSite: 'lax',
       domain: `.${env.NEXT_PUBLIC_BASE_DOMAIN}`,
       expires: 1,
     })
 
-    console.log('[AuthProvider] refreshJwt => JWT refreshed:', jwt)
+    if (process.env.NODE_ENV !== 'production') {
+      console.log('[AuthProvider] refreshJwt => JWT refreshed:', jwt)
+    }
 
     return jwt
-  }, [signOut])
+  }, [signOut])
```

This removes the ineffective `httpOnly`, adds error handling, and suppresses token logs in production.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const refreshJwt = useCallback(async (): Promise<string | void> => {
    const { data, error } = await authClient.getSession()

    console.log('[AuthProvider] refreshJwt', data, error)

    if (!data || error) {
      return signOut()
    }

    let res: Response
    try {
      res = await fetch(`${env.NEXT_PUBLIC_API_URL}/auth/token`, {
        method: 'GET',
        headers: {
          Authorization: `Bearer ${data.session.token}`,
        },
      })
    } catch (e) {
      console.error('[AuthProvider] refreshJwt fetch failed', e)
      return signOut()
    }

    if (!res.ok) {
      return signOut()
    }

    const { jwt } = (await res.json()) as { jwt: string }

    Cookies.set('avelin.session_jwt', jwt, {
      path: '/',
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      domain: `.${env.NEXT_PUBLIC_BASE_DOMAIN}`,
      expires: 1,
    })

    if (process.env.NODE_ENV !== 'production') {
      console.log('[AuthProvider] refreshJwt => JWT refreshed:', jwt)
    }

    return jwt
  }, [signOut])
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T02:52:05Z
author_association: NONE

PR comments:
user: login: linear[bot]
created_at: 2025-05-08T02:48:01Z
body: <p><a href="https://linear.app/bazzalabs/issue/AVELIN-251/jwt-is-not-refreshed-after-sign-in">AVELIN-251 JWT is not refreshed after sign-in</a></p>
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-08T02:48:05Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This set of changes introduces a unified JWT refresh flow across the authentication and Zero client layers. The `refreshJwt` function is now implemented in the authentication provider and injected into the Zero client and provider, replacing previous token refresh logic. Cookie handling for JWT tokens is standardized, and JWT expiration is consistently managed.

## Changes

| File(s)                                                                 | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
|-------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| apps/web/lib/zero.ts                                                    | Refactored to require a `refreshJwt` callback for token refresh; removed old token refresh logic; added debug logs; introduced `clearZeroClient` to reset the singleton client.                                                                                                                                                                                                                                                                                                                                                                                                            |
| apps/web/providers/auth-provider.tsx                                    | Added `refreshJwt` async method to the auth context; implemented JWT refresh via API and cookie setting; updated context and types to expose `refreshJwt`.                                                                                                                                                                                                                                                                                                                                                                                          |
| apps/web/providers/zero-provider.tsx                                    | Imported and injected `refreshJwt` from auth provider into Zero client initialization; removed JWT payload debug logging.                                                                                                                                                                                                                                                                                                                                                                                     |
| apps/web/middleware.ts                                                  | Improved JWT cookie handling by converting the string `'undefined'` to actual `undefined`; standardized JWT expiration handling using JavaScript `Date` objects for cookie expiry.                                                                                                                                                                                                                                                                                                                           |
| packages/auth/src/index.ts                                              | Defined reusable JWT options; added middleware to issue JWT and set it as a cookie after `/callback`; standardized cookie attributes and JWT generation.                                                                                                                                                                                                                                                                                                                                                      |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant AuthProvider
    participant ZeroProvider
    participant ZeroClient
    participant API

    User->>AuthProvider: Needs authenticated action
    AuthProvider->>ZeroProvider: Provides refreshJwt function
    ZeroProvider->>ZeroClient: Initializes with jwt, payload, refreshJwt
    ZeroClient->>ZeroClient: Checks JWT validity
    alt JWT missing/invalid
        ZeroClient->>AuthProvider: Calls refreshJwt()
        AuthProvider->>API: GET /auth/token with session token
        API-->>AuthProvider: Returns new JWT
        AuthProvider->>ZeroClient: Returns refreshed JWT
        ZeroClient->>ZeroClient: Uses refreshed JWT
    else JWT valid
        ZeroClient->>ZeroClient: Uses existing JWT
    end
```

## Poem

> 🥕  
> A token hops from paw to paw,  
> Refreshed anew by rabbit law.  
> Cookies crisp, with time precise,  
> JWTs bounce—oh, isn't that nice!  
> Providers share, the Zero's clear,  
> Secure and sweet, our code this year.  
> 🐇

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b01c1532169d1fb9739bda19001a54586c61ea80 and 835d3b9b6d453618369fb99d820cc4b53d5b694b.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `apps/web/lib/zero.ts` (3 hunks)
* `apps/web/middleware.ts` (3 hunks)
* `apps/web/providers/auth-provider.tsx` (5 hunks)
* `apps/web/providers/zero-provider.tsx` (1 hunks)
* `packages/auth/src/index.ts` (4 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (5)</summary>

* apps/web/providers/zero-provider.tsx
* apps/web/lib/zero.ts
* apps/web/middleware.ts
* packages/auth/src/index.ts
* apps/web/providers/auth-provider.tsx

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=avelinapp/avelin&utm_content=269):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: kianbazza
created_at: 2025-05-08T02:48:13Z
body: * **#270** <a href="https://app.graphite.dev/github/pr/avelinapp/avelin/270?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#269** <a href="https://app.graphite.dev/github/pr/avelinapp/avelin/269?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/avelinapp/avelin/269?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* **#271** <a href="https://app.graphite.dev/github/pr/avelinapp/avelin/271?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* `main`


<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: COLLABORATOR
user: login: kianbazza
created_at: 2025-05-08T03:00:08Z
body: ### Merge activity

* **May 7, 11:00 PM EDT**: A user started a stack merge that includes this pull request via [Graphite](https://app.graphite.dev/github/pr/avelinapp/avelin/269).
* **May 7, 11:01 PM EDT**: [Graphite](https://app.graphite.dev/github/pr/avelinapp/avelin/269) rebased this pull request as part of a merge.
* **May 7, 11:02 PM EDT**: @kianbazza merged this pull request with [Graphite](https://app.graphite.dev/github/pr/avelinapp/avelin/269).

author_association: COLLABORATOR