From bd17bb45c46e5e22520f90f32c0833946bada062 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 23 Feb 2026 10:35:08 +0100 Subject: [PATCH 01/12] chore: update CHANGELOG.md for version 0.11.1-alpha, highlighting secure sign-in improvements and update package version --- CHANGELOG.md | 9 +++++- app/actions/auth.ts | 4 +-- app/auth/callback/page.tsx | 62 +++++++++----------------------------- package.json | 2 +- 4 files changed, 25 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06558e7..d090467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [0.11.1-alpha] - 2026-02-23 + +### Changed + +- **Safer sign-in from the Ciphera hub.** When you open Pulse from the Ciphera Apps page, your credentials are no longer visible in the browser address bar. Sign-in now uses a secure one-time code that expires in seconds, so your session stays private even if someone sees your screen or browser history. + ## [0.11.0-alpha] - 2026-02-22 ### Added @@ -168,7 +174,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), --- -[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.11.0-alpha...HEAD +[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.11.1-alpha...HEAD +[0.11.1-alpha]: https://github.com/ciphera-net/pulse/compare/v0.11.0-alpha...v0.11.1-alpha [0.11.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.10.0-alpha...v0.11.0-alpha [0.10.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.9.0-alpha...v0.10.0-alpha [0.9.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.8.0-alpha...v0.9.0-alpha diff --git a/app/actions/auth.ts b/app/actions/auth.ts index 869cfff..dc38c5f 100644 --- a/app/actions/auth.ts +++ b/app/actions/auth.ts @@ -33,7 +33,7 @@ interface UserPayload { /** Error type returned to client for mapping to user-facing copy (no sensitive details). */ export type AuthExchangeErrorType = 'network' | 'expired' | 'invalid' | 'server' -export async function exchangeAuthCode(code: string, codeVerifier: string, redirectUri: string) { +export async function exchangeAuthCode(code: string, codeVerifier: string | null, redirectUri: string) { try { const res = await fetch(`${AUTH_API_URL}/oauth/token`, { method: 'POST', @@ -45,7 +45,7 @@ export async function exchangeAuthCode(code: string, codeVerifier: string, redir code, client_id: 'pulse-app', redirect_uri: redirectUri, - code_verifier: codeVerifier, + code_verifier: codeVerifier || '', }), }) diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx index b2a8c34..1e69707 100644 --- a/app/auth/callback/page.tsx +++ b/app/auth/callback/page.tsx @@ -5,7 +5,7 @@ import { logger } from '@/lib/utils/logger' import { useRouter, useSearchParams } from 'next/navigation' import { useAuth } from '@/lib/auth/context' import { AUTH_URL, default as apiRequest } from '@/lib/api/client' -import { exchangeAuthCode, setSessionAction } from '@/app/actions/auth' +import { exchangeAuthCode } from '@/app/actions/auth' import { authMessageFromErrorType, type AuthErrorType } from '@ciphera-net/ui' import { LoadingOverlay } from '@ciphera-net/ui' @@ -21,7 +21,7 @@ function AuthCallbackContent() { const code = searchParams.get('code') const codeVerifier = localStorage.getItem('oauth_code_verifier') const redirectUri = typeof window !== 'undefined' ? window.location.origin + '/auth/callback' : '' - if (!code || !codeVerifier) return + if (!code) return const result = await exchangeAuthCode(code, codeVerifier, redirectUri) if (result.success && result.user) { // * Fetch full profile (including display_name) before navigating so header shows correct name on first paint @@ -47,59 +47,25 @@ function AuthCallbackContent() { }, [searchParams, login, router]) useEffect(() => { - // * Prevent double execution (React Strict Mode or fast re-renders) if (processedRef.current && !isRetrying) return - // * Check for direct token passing (from auth-frontend direct login) - // * This flow exposes tokens in URL, kept for legacy support. - // * Recommended: Use Authorization Code flow (below) - const token = searchParams.get('token') - const refreshToken = searchParams.get('refresh_token') - - if (token && refreshToken) { - processedRef.current = true - const handleDirectTokens = async () => { - const result = await setSessionAction(token, refreshToken) - if (result.success && result.user) { - // * Fetch full profile (including display_name) before navigating so header shows correct name on first paint - try { - const fullProfile = await apiRequest<{ id: string; email: string; display_name?: string; totp_enabled: boolean; org_id?: string; role?: string }>('/auth/user/me') - const merged = { ...fullProfile, org_id: result.user.org_id ?? fullProfile.org_id, role: result.user.role ?? fullProfile.role } - login(merged) - } catch { - login(result.user) - } - if (typeof window !== 'undefined' && localStorage.getItem('pulse_pending_checkout')) { - router.push('/welcome') - } else { - const raw = searchParams.get('returnTo') || '/' - const safe = (typeof raw === 'string' && raw.startsWith('/') && !raw.startsWith('//')) ? raw : '/' - router.push(safe) - } - } else { - setError(authMessageFromErrorType('invalid')) - } - } - handleDirectTokens() - return - } - const code = searchParams.get('code') + if (!code) return + const state = searchParams.get('state') - - if (!code || !state) return - const storedState = localStorage.getItem('oauth_state') const codeVerifier = localStorage.getItem('oauth_code_verifier') - if (!codeVerifier) { - setError('Missing code verifier') - return - } - if (state !== storedState) { - logger.error('State mismatch', { received: state, stored: storedState }) - setError('Invalid state') - return + // * Full OAuth flow (app-initiated): validate state + use PKCE + // * Session-authorized flow (from auth hub): no stored state or verifier + const isFullOAuth = !!storedState && !!codeVerifier + + if (isFullOAuth) { + if (state !== storedState) { + logger.error('State mismatch', { received: state, stored: storedState }) + setError('Invalid state') + return + } } processedRef.current = true diff --git a/package.json b/package.json index 022fa33..50a3275 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-frontend", - "version": "0.11.0-alpha", + "version": "0.11.1-alpha", "private": true, "scripts": { "dev": "next dev", From 2889b0bb0a05f0813511e08676549d89c3766ece Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 23 Feb 2026 10:57:11 +0100 Subject: [PATCH 02/12] chore: update @ciphera-net/ui to 0.0.59 for improved functionality --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb8f7d6..49c6b2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,14 @@ { "name": "pulse-frontend", - "version": "0.9.0-alpha", + "version": "0.11.1-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-frontend", - "version": "0.9.0-alpha", + "version": "0.11.1-alpha", "dependencies": { - "@ciphera-net/ui": "^0.0.58", + "@ciphera-net/ui": "^0.0.59", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@stripe/react-stripe-js": "^5.6.0", @@ -1541,9 +1541,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.0.58", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.58/ac48a989da2db79880ce2fa7f89b63a62e2b68c9", - "integrity": "sha512-cvptYjs+E72EQvM5YGx5pp4SOiyJ7t5qv5NSRfoFxtcTCwR4sKUN4SoZUA+HV3tLlq4qXXHAB98E7qgbBRIn+Q==", + "version": "0.0.59", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.59/220eabb8186f92af5f38d26f6a6515fd55f2650c", + "integrity": "sha512-HFjtTmeljbEroDJhkHV200cwVRW1qAzymBiwYErqF4J5W21GN+gfY4w31AHCjSsZgmNMOEprvqZp3ll2wwGcKg==", "dependencies": { "@radix-ui/react-icons": "^1.3.0", "clsx": "^2.1.0", diff --git a/package.json b/package.json index 50a3275..8c304d3 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@ciphera-net/ui": "^0.0.58", + "@ciphera-net/ui": "^0.0.59", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@stripe/react-stripe-js": "^5.6.0", From b54af6c03a0feca0c69249ce2349edf6252e68a4 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 23 Feb 2026 11:35:02 +0100 Subject: [PATCH 03/12] fix: require password confirmation to disable 2FA, enhancing security against session hijacking --- CHANGELOG.md | 4 ++++ lib/api/2fa.ts | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d090467..8797068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +### Fixed + +- **2FA disable now requires password confirmation.** Disabling 2FA sends the derived password to the backend for verification. This prevents an attacker with a hijacked session from stripping 2FA. + ## [0.11.1-alpha] - 2026-02-23 ### Changed diff --git a/lib/api/2fa.ts b/lib/api/2fa.ts index e2a7570..da27c8e 100644 --- a/lib/api/2fa.ts +++ b/lib/api/2fa.ts @@ -27,9 +27,10 @@ export async function verify2FA(code: string): Promise { }) } -export async function disable2FA(): Promise { +export async function disable2FA(passwordDerived: string): Promise { return apiRequest('/auth/2fa/disable', { method: 'POST', + body: JSON.stringify({ password: passwordDerived }), }) } From 27b3aa83809cf96a94899048f98cc68763f61439 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 23 Feb 2026 11:43:57 +0100 Subject: [PATCH 04/12] feat: add 2FA recovery codes regeneration and backup functionality, enhancing account security --- CHANGELOG.md | 4 ++++ lib/api/2fa.ts | 3 ++- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8797068..ad766c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +### Added + +- **2FA recovery codes backup.** When you enable 2FA, you receive recovery codes. You can now regenerate new codes (with password confirmation) from Settings and download them as a `.txt` file. Regenerating invalidates all existing codes. + ### Fixed - **2FA disable now requires password confirmation.** Disabling 2FA sends the derived password to the backend for verification. This prevents an attacker with a hijacked session from stripping 2FA. diff --git a/lib/api/2fa.ts b/lib/api/2fa.ts index da27c8e..b8247d4 100644 --- a/lib/api/2fa.ts +++ b/lib/api/2fa.ts @@ -34,8 +34,9 @@ export async function disable2FA(passwordDerived: string): Promise { }) } -export async function regenerateRecoveryCodes(): Promise { +export async function regenerateRecoveryCodes(passwordDerived: string): Promise { return apiRequest('/auth/2fa/recovery', { method: 'POST', + body: JSON.stringify({ password: passwordDerived }), }) } diff --git a/package-lock.json b/package-lock.json index 49c6b2f..92934ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "pulse-frontend", "version": "0.11.1-alpha", "dependencies": { - "@ciphera-net/ui": "^0.0.59", + "@ciphera-net/ui": "^0.0.60", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@stripe/react-stripe-js": "^5.6.0", @@ -1541,9 +1541,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.0.59", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.59/220eabb8186f92af5f38d26f6a6515fd55f2650c", - "integrity": "sha512-HFjtTmeljbEroDJhkHV200cwVRW1qAzymBiwYErqF4J5W21GN+gfY4w31AHCjSsZgmNMOEprvqZp3ll2wwGcKg==", + "version": "0.0.60", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.60/8d3b666ea855e202cf55fa6bdf7553c843635203", + "integrity": "sha512-993Zsc4TGYrjO7cG4Q7oskgo0U+fEY4s8mDmR/jhdmZQv83bNXG9YgjvWcePhojhsVf+Nyo1DA2Nm0j/fwAzaA==", "dependencies": { "@radix-ui/react-icons": "^1.3.0", "clsx": "^2.1.0", diff --git a/package.json b/package.json index 8c304d3..975c7f4 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@ciphera-net/ui": "^0.0.59", + "@ciphera-net/ui": "^0.0.60", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@stripe/react-stripe-js": "^5.6.0", From dd9d4c5ac2eb1e1ce09013ba35c3cf49c6e3dfea Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 23 Feb 2026 18:04:10 +0100 Subject: [PATCH 05/12] chore: update @ciphera-net/ui to version 0.0.61 for improved functionality --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 92934ae..1fa0a79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "pulse-frontend", "version": "0.11.1-alpha", "dependencies": { - "@ciphera-net/ui": "^0.0.60", + "@ciphera-net/ui": "^0.0.61", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@stripe/react-stripe-js": "^5.6.0", @@ -1541,9 +1541,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.0.60", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.60/8d3b666ea855e202cf55fa6bdf7553c843635203", - "integrity": "sha512-993Zsc4TGYrjO7cG4Q7oskgo0U+fEY4s8mDmR/jhdmZQv83bNXG9YgjvWcePhojhsVf+Nyo1DA2Nm0j/fwAzaA==", + "version": "0.0.61", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.61/b9a3b8917090c640655b0fd1c9dfe9c5a1ea0421", + "integrity": "sha512-yLhint7P2UfCjkYOXmRAGrgoswTSZUyV9AJGMPVP7dbdWhir1Mn3FkI54GdBldeI/QzTMYE5s4kSp8dKjTMiLw==", "dependencies": { "@radix-ui/react-icons": "^1.3.0", "clsx": "^2.1.0", diff --git a/package.json b/package.json index 975c7f4..9ac7c0c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@ciphera-net/ui": "^0.0.60", + "@ciphera-net/ui": "^0.0.61", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@stripe/react-stripe-js": "^5.6.0", From f62d142adb33cbe2e0e9ced9ea583ae9391de026 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 23 Feb 2026 18:46:46 +0100 Subject: [PATCH 06/12] fix: resolve sign-in issue after inactivity by ensuring only valid access tokens trigger redirects, improving user experience --- CHANGELOG.md | 1 + middleware.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad766c7..f27cce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Fixed +- **Sign in after inactivity.** Clicking "Sign in" after a period of inactivity no longer does nothing. Previously, stale refresh cookies caused the middleware to redirect away from the login page; now only a valid access token triggers that redirect, so you can complete OAuth sign-in when your session has expired. - **2FA disable now requires password confirmation.** Disabling 2FA sends the derived password to the backend for verification. This prevents an attacker with a hijacked session from stripping 2FA. ## [0.11.1-alpha] - 2026-02-23 diff --git a/middleware.ts b/middleware.ts index 440ccf1..9ab1a7c 100644 --- a/middleware.ts +++ b/middleware.ts @@ -34,8 +34,9 @@ export function middleware(request: NextRequest) { const hasRefresh = request.cookies.has('refresh_token') const hasSession = hasAccess || hasRefresh - // * Authenticated user hitting /login or /signup → send them home - if (hasSession && AUTH_ONLY_ROUTES.has(pathname)) { + // * Authenticated user (with access token) hitting /login or /signup → send them home. + // * Only check access_token; stale refresh_token alone must not block login (fixes post-inactivity sign-in). + if (hasAccess && AUTH_ONLY_ROUTES.has(pathname)) { return NextResponse.redirect(new URL('/', request.url)) } From 3cb5416251942e6f13a953b4fd2ab9a47578f71c Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 23 Feb 2026 18:57:03 +0100 Subject: [PATCH 07/12] fix: implement automatic token refresh to prevent frequent re-logins, enhancing user experience during inactivity --- CHANGELOG.md | 1 + lib/auth/context.tsx | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f27cce3..6f9a4b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Fixed - **Sign in after inactivity.** Clicking "Sign in" after a period of inactivity no longer does nothing. Previously, stale refresh cookies caused the middleware to redirect away from the login page; now only a valid access token triggers that redirect, so you can complete OAuth sign-in when your session has expired. +- **Frequent re-login.** You no longer have to sign in multiple times a day. When the access token expires after 15 minutes of inactivity, the app now automatically refreshes it using your refresh token on the next page load, so you stay logged in for up to 30 days. - **2FA disable now requires password confirmation.** Disabling 2FA sends the derived password to the backend for verification. This prevents an attacker with a hijacked session from stripping 2FA. ## [0.11.1-alpha] - 2026-02-23 diff --git a/lib/auth/context.tsx b/lib/auth/context.tsx index 537732b..a8b5810 100644 --- a/lib/auth/context.tsx +++ b/lib/auth/context.tsx @@ -110,8 +110,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { useEffect(() => { const init = async () => { // * 1. Check server-side session (cookies) - const session = await getSessionAction() - + let session = await getSessionAction() + + // * 2. If no access_token but refresh_token may exist, try refresh (fixes 15-min inactivity logout) + if (!session && typeof window !== 'undefined') { + const refreshRes = await fetch('/api/auth/refresh', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + }) + if (refreshRes.ok) { + session = await getSessionAction() + } + } + if (session) { setUser(session) localStorage.setItem('user', JSON.stringify(session)) @@ -129,7 +141,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { localStorage.removeItem('user') setUser(null) } - + // * Clear legacy tokens if they exist (migration) if (localStorage.getItem('token')) { localStorage.removeItem('token') From 6fb4da5a6922fedb511b6274c26cca12e17bd282 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 23 Feb 2026 19:00:08 +0100 Subject: [PATCH 08/12] chore: update @ciphera-net/ui dependency to version 0.0.62 in package.json and package-lock.json --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1fa0a79..e926f1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "pulse-frontend", "version": "0.11.1-alpha", "dependencies": { - "@ciphera-net/ui": "^0.0.61", + "@ciphera-net/ui": "^0.0.62", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@stripe/react-stripe-js": "^5.6.0", @@ -1541,9 +1541,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.0.61", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.61/b9a3b8917090c640655b0fd1c9dfe9c5a1ea0421", - "integrity": "sha512-yLhint7P2UfCjkYOXmRAGrgoswTSZUyV9AJGMPVP7dbdWhir1Mn3FkI54GdBldeI/QzTMYE5s4kSp8dKjTMiLw==", + "version": "0.0.62", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.62/9cc42f0ed1d765dcf2b2e31b6db6de2393544c94", + "integrity": "sha512-WCV2exxhiMAwR0AtQiYzp1kzYZhjMx5fajw0cFniG1F6raNLmDjvb0bILPjSvOoqw8uixlRZgxAke3ovW5wfcQ==", "dependencies": { "@radix-ui/react-icons": "^1.3.0", "clsx": "^2.1.0", diff --git a/package.json b/package.json index 9ac7c0c..b8f90ea 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@ciphera-net/ui": "^0.0.61", + "@ciphera-net/ui": "^0.0.62", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@stripe/react-stripe-js": "^5.6.0", From ef041d9a011b641f88eb602fc384906ab574760b Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 23 Feb 2026 19:10:07 +0100 Subject: [PATCH 09/12] chore: update @ciphera-net/ui dependency to version 0.0.63 in package.json --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index e926f1b..90d561f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "pulse-frontend", "version": "0.11.1-alpha", "dependencies": { - "@ciphera-net/ui": "^0.0.62", + "@ciphera-net/ui": "^0.0.63", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@stripe/react-stripe-js": "^5.6.0", @@ -1541,9 +1541,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.0.62", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.62/9cc42f0ed1d765dcf2b2e31b6db6de2393544c94", - "integrity": "sha512-WCV2exxhiMAwR0AtQiYzp1kzYZhjMx5fajw0cFniG1F6raNLmDjvb0bILPjSvOoqw8uixlRZgxAke3ovW5wfcQ==", + "version": "0.0.63", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.63/70ae8faf2233a7d654f57767d11ac1c12f3e3a74", + "integrity": "sha512-E1yeKX5OG/7naK5ug5UGwJUmRAA7qKfF5YHiOhKibETpqlpWdehZTUrMygMP7FSho2VGFHzM+dHRy7QlxtsOGA==", "dependencies": { "@radix-ui/react-icons": "^1.3.0", "clsx": "^2.1.0", diff --git a/package.json b/package.json index b8f90ea..cc1b03c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@ciphera-net/ui": "^0.0.62", + "@ciphera-net/ui": "^0.0.63", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@stripe/react-stripe-js": "^5.6.0", From 1484ade7173048536bddf0dffe96917314011e4f Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 23 Feb 2026 19:33:49 +0100 Subject: [PATCH 10/12] chore: update @ciphera-net/ui dependency to version 0.0.64 in package.json and package-lock.json --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 90d561f..b1fc630 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "pulse-frontend", "version": "0.11.1-alpha", "dependencies": { - "@ciphera-net/ui": "^0.0.63", + "@ciphera-net/ui": "^0.0.64", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@stripe/react-stripe-js": "^5.6.0", @@ -1541,9 +1541,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.0.63", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.63/70ae8faf2233a7d654f57767d11ac1c12f3e3a74", - "integrity": "sha512-E1yeKX5OG/7naK5ug5UGwJUmRAA7qKfF5YHiOhKibETpqlpWdehZTUrMygMP7FSho2VGFHzM+dHRy7QlxtsOGA==", + "version": "0.0.64", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.64/1630605518a705ba9e74f003b9c66646bcc699ac", + "integrity": "sha512-xY+yALuCqWtsH78t6xmy2JQnQ8WFSlihElHnetFr6GQp9mOTCA5rlQq+a8hyg4xW7uXtIbKmPBxVnH5TlH9lBQ==", "dependencies": { "@radix-ui/react-icons": "^1.3.0", "clsx": "^2.1.0", diff --git a/package.json b/package.json index cc1b03c..14ec466 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@ciphera-net/ui": "^0.0.63", + "@ciphera-net/ui": "^0.0.64", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@stripe/react-stripe-js": "^5.6.0", From 801dc1d7732c1b5ef4a3dbf80d823396f85c47cb Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 23 Feb 2026 20:18:18 +0100 Subject: [PATCH 11/12] chore: add @simplewebauthn/browser dependency to package.json and package-lock.json for WebAuthn support --- components/settings/ProfileSettings.tsx | 4 ++ lib/api/webauthn.ts | 54 +++++++++++++++++++++++++ package-lock.json | 7 ++++ package.json | 1 + 4 files changed, 66 insertions(+) create mode 100644 lib/api/webauthn.ts diff --git a/components/settings/ProfileSettings.tsx b/components/settings/ProfileSettings.tsx index 6713140..56a91af 100644 --- a/components/settings/ProfileSettings.tsx +++ b/components/settings/ProfileSettings.tsx @@ -6,6 +6,7 @@ import api from '@/lib/api/client' import { deriveAuthKey } from '@/lib/crypto/password' import { deleteAccount, getUserSessions, revokeSession, updateUserPreferences, updateDisplayName } from '@/lib/api/user' import { setup2FA, verify2FA, disable2FA, regenerateRecoveryCodes } from '@/lib/api/2fa' +import { registerPasskey, listPasskeys, deletePasskey } from '@/lib/api/webauthn' export default function ProfileSettings() { const { user, refresh, logout } = useAuth() @@ -46,6 +47,9 @@ export default function ProfileSettings() { onRegenerateRecoveryCodes={regenerateRecoveryCodes} onGetSessions={getUserSessions} onRevokeSession={revokeSession} + onRegisterPasskey={registerPasskey} + onListPasskeys={listPasskeys} + onDeletePasskey={deletePasskey} onUpdatePreferences={updateUserPreferences} deriveAuthKey={deriveAuthKey} refreshUser={refresh} diff --git a/lib/api/webauthn.ts b/lib/api/webauthn.ts new file mode 100644 index 0000000..e3f2fe8 --- /dev/null +++ b/lib/api/webauthn.ts @@ -0,0 +1,54 @@ +/** + * WebAuthn / Passkey API client for settings (list, register, delete). + */ + +import { startRegistration, type PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/browser' +import apiRequest from './client' + +export interface BeginRegistrationResponse { + sessionId: string + creationOptions: { + publicKey: Record + mediation?: string + } +} + +export interface PasskeyCredential { + id: string + createdAt: string +} + +export interface ListPasskeysResponse { + credentials: PasskeyCredential[] +} + +export async function registerPasskey(): Promise { + const { sessionId, creationOptions } = await apiRequest( + '/auth/webauthn/register/begin', + { method: 'POST' } + ) + const optionsJSON = creationOptions?.publicKey + if (!optionsJSON) { + throw new Error('Invalid registration options') + } + const response = await startRegistration({ + optionsJSON: optionsJSON as unknown as PublicKeyCredentialCreationOptionsJSON, + }) + await apiRequest<{ message: string }>('/auth/webauthn/register/finish', { + method: 'POST', + body: JSON.stringify({ sessionId, response }), + }) +} + +export async function listPasskeys(): Promise { + return apiRequest('/auth/webauthn/credentials', { + method: 'GET', + }) +} + +export async function deletePasskey(credentialId: string): Promise { + return apiRequest( + `/auth/webauthn/credentials/${encodeURIComponent(credentialId)}`, + { method: 'DELETE' } + ) +} diff --git a/package-lock.json b/package-lock.json index b1fc630..3ca446f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@ciphera-net/ui": "^0.0.64", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", + "@simplewebauthn/browser": "^13.2.2", "@stripe/react-stripe-js": "^5.6.0", "@stripe/stripe-js": "^8.7.0", "axios": "^1.13.2", @@ -2717,6 +2718,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@simplewebauthn/browser": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.2.tgz", + "integrity": "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==", + "license": "MIT" + }, "node_modules/@stripe/react-stripe-js": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-5.6.0.tgz", diff --git a/package.json b/package.json index 14ec466..f8d66ec 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@ciphera-net/ui": "^0.0.64", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", + "@simplewebauthn/browser": "^13.2.2", "@stripe/react-stripe-js": "^5.6.0", "@stripe/stripe-js": "^8.7.0", "axios": "^1.13.2", From 2cb8ffddecfdce797a7710c587c80b405991851d Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Wed, 25 Feb 2026 12:41:18 +0100 Subject: [PATCH 12/12] chore: update CHANGELOG.md to include new features, improvements, and fixes for performance insights, goals tracking, and enhanced error handling --- CHANGELOG.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f9a4b8..5e1f285 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added +- **Performance insights.** Track how fast your site loads with Core Web Vitals (page load speed, layout shifts, responsiveness). Turn it on in Site Settings → Data & Privacy to see a performance widget on your dashboard. +- **Goals & Events.** Define custom goals (e.g. signup, purchase) and track them with `pulse.track()` in your snippet. Counts appear on your dashboard once you add goals in Site Settings → Goals & Events. - **2FA recovery codes backup.** When you enable 2FA, you receive recovery codes. You can now regenerate new codes (with password confirmation) from Settings and download them as a `.txt` file. Regenerating invalidates all existing codes. ### Fixed @@ -34,10 +36,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Better form experience.** Forms now auto-focus the first field when they open, text inputs enforce character limits with a visible counter when you're close, and the settings page warns you before navigating away with unsaved changes. - **Accessibility improvements.** The notification bell, workspace switcher, and all dashboard tabs are now fully keyboard-navigable. Screen readers announce unread counts, active organizations, and tab changes correctly. Decorative icons are hidden from assistive technology. - **Smooth organization switching.** Switching between organizations now shows a branded loading screen instead of a blank flash while the page reloads. -- **Graceful server shutdown.** Deployments no longer kill in-flight requests or interrupt background tasks. The server finishes ongoing work before shutting down. -- **Database connection pooling.** The backend now limits and recycles database connections, preventing exhaustion under load and reducing query latency. -- **Date range validation.** Analytics, funnel, and uptime queries now reject invalid date ranges (end before start, or spans longer than a year) instead of silently returning empty or oversized results. -- **Excluded paths limit.** Sites can now have up to 50 excluded paths. Previously there was no cap, which could slow down event processing. +- **Graceful server shutdown.** Deployments no longer kill in-flight requests or interrupt background tasks. The server finishes ongoing work before shutting down, so your active sessions aren't cut off mid-action. +- **Database connection pooling.** The backend now limits and recycles database connections, preventing exhaustion under load and keeping queries fast even with many concurrent users. +- **Date range validation.** Analytics, funnel, and uptime queries now reject invalid date ranges (end before start, or spans longer than a year) and show a clear error instead of empty or confusing results. +- **Excluded paths limit.** Sites can now have up to 50 excluded paths. Previously there was no cap, which could slow down event processing; the limit keeps things fast while still giving you flexibility. ### Changed @@ -45,8 +47,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Clearer error messages.** When something goes wrong, the error message now tells you what failed (e.g. "Failed to load uptime monitors") instead of a generic "Failed to load data". - **Faster favicon loading.** Site icons in the dashboard, referrers, and campaigns now use Next.js image optimization for better caching and lazy loading. - **Tighter name limits.** Site, funnel, and monitor names are now capped at 100 characters instead of 255 — long enough for any real name, short enough to not break the UI. -- **Stricter type safety.** Eliminated all `any` types and `@ts-ignore` suppressions across the codebase, so the TypeScript compiler catches more bugs at build time. -- **Smaller page downloads.** Icon imports are now tree-shaken so only the icons actually used are included in the bundle. +- **Stricter type safety.** Eliminated all `any` types and `@ts-ignore` suppressions across the codebase, so the TypeScript compiler catches more bugs at build time and fewer edge cases slip through. +- **Smaller page downloads.** Icon imports are now tree-shaken so only the icons actually used are included in the bundle, reducing download size and speeding up page loads. - **Removed debug logs.** Auth and organization-switching details no longer leak into the browser console in production. Error logs are now also suppressed in production and only appear during development. ### Fixed