/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import type { QueryClient } from '@tanstack/react-query'
import axios from 'axios'
import { t } from 'i18next'
import { publishAuthSessionEvent } from '@/lib/auth-session-sync'
import {
useAuthStore,
type AuthBootstrapState,
type AuthBundle,
type AuthUser,
type LoginSession,
} from '@/stores/auth-store'
export type RefreshOutcome =
| { kind: 'authenticated'; bundle: AuthBundle }
| { kind: 'anonymous' }
| { kind: 'transient_error'; error: unknown }
| { kind: 'out_of_sync'; code?: string }
export interface AuthRefreshHTTPResponse {
status: number
data?: unknown
error?: unknown
}
export interface AuthRefreshRuntime {
request: (expectedSID?: string) => Promise
getExpectedSID: () => string | undefined
parseBundle: (value: unknown) => AuthBundle | null
acceptBundle: (bundle: AuthBundle) => void
clear: (synchronizeTabs: boolean, bootstrapState?: AuthBootstrapState) => void
markTransient: () => void
wait: (delay: number) => Promise
isCurrent?: () => boolean
}
export interface AuthTokenRotation {
access_token: string
token_type: string
access_expires_at: number
session: LoginSession
}
export class AuthRotationError extends Error {
constructor(message: string) {
super(message)
this.name = 'AuthRotationError'
}
}
const authClient = axios.create({
baseURL: '',
withCredentials: true,
headers: {
'Cache-Control': 'no-store',
},
})
const refreshRaceDelays = [80, 200, 500] as const
let refreshPromise: Promise | null = null
let authEpoch = 0
class AuthRefreshSupersededError extends Error {
constructor() {
super('Authentication refresh was superseded')
this.name = 'AuthRefreshSupersededError'
}
}
function isRecord(value: unknown): value is Record {
return Boolean(value) && typeof value === 'object'
}
function isAuthUser(value: unknown): value is AuthUser {
if (!isRecord(value)) return false
return (
Number.isInteger(value.id) &&
Number(value.id) > 0 &&
typeof value.username === 'string' &&
typeof value.role === 'number'
)
}
function isLoginSession(value: unknown): value is LoginSession {
if (!isRecord(value)) return false
return (
typeof value.sid === 'string' &&
value.sid.length > 0 &&
typeof value.current === 'boolean' &&
typeof value.login_method === 'string' &&
typeof value.ip === 'string' &&
typeof value.user_agent === 'string' &&
typeof value.created_at === 'number' &&
typeof value.last_active_at === 'number' &&
typeof value.expires_at === 'number'
)
}
function hasValidTokenFields(value: Record): boolean {
return (
typeof value.access_token === 'string' &&
value.access_token.length > 0 &&
typeof value.token_type === 'string' &&
value.token_type.length > 0 &&
typeof value.access_expires_at === 'number' &&
Number.isFinite(value.access_expires_at) &&
value.access_expires_at > 0
)
}
export function isAuthBundle(value: unknown): value is AuthBundle {
if (!isRecord(value)) return false
return (
hasValidTokenFields(value) &&
isAuthUser(value.user) &&
isLoginSession(value.session)
)
}
function isAuthTokenRotation(value: unknown): value is AuthTokenRotation {
return (
isRecord(value) &&
hasValidTokenFields(value) &&
value.token_type === 'Bearer' &&
isLoginSession(value.session) &&
value.session.current
)
}
export function applyAuthBundle(
bundle: AuthBundle,
synchronizeTabs = true
): void {
const previousSID = useAuthStore.getState().auth.session?.sid
authEpoch += 1
useAuthStore.getState().auth.setBundle(bundle)
if (synchronizeTabs && previousSID !== bundle.session.sid) {
publishAuthSessionEvent('authenticated', bundle.session.sid)
}
}
export function applyAuthRotation(value: unknown): void {
if (!isAuthTokenRotation(value)) {
throw new AuthRotationError('Invalid authentication rotation response')
}
const auth = useAuthStore.getState().auth
if (!auth.user || !auth.session) {
throw new AuthRotationError('Authentication rotation has no active session')
}
if (value.session.sid !== auth.session.sid) {
throw new AuthRotationError('Authentication rotation session mismatch')
}
applyAuthBundle(
{
access_token: value.access_token,
token_type: value.token_type,
access_expires_at: value.access_expires_at,
session: value.session,
user: auth.user,
},
false
)
}
export function clearAuthentication(
synchronizeTabs = true,
bootstrapState: AuthBootstrapState = 'complete'
): void {
const sid = useAuthStore.getState().auth.session?.sid
authEpoch += 1
useAuthStore.getState().auth.reset(bootstrapState)
if (synchronizeTabs && sid) {
publishAuthSessionEvent('signed_out', sid)
}
}
export function clearAuthenticatedClientState(
queryClient: QueryClient,
synchronizeTabs = true
): void {
queryClient.clear()
clearAuthentication(synchronizeTabs)
}
function waitForRefreshRace(delay: number): Promise {
return new Promise((resolve) => globalThis.setTimeout(resolve, delay))
}
export function createRefreshRunner(
runtime: AuthRefreshRuntime
): () => Promise {
const superseded = (): RefreshOutcome => ({
kind: 'transient_error',
error: new AuthRefreshSupersededError(),
})
const run = async (
raceAttempt: number,
allowMismatchRetry: boolean
): Promise => {
if (runtime.isCurrent && !runtime.isCurrent()) return superseded()
const response = await runtime.request(runtime.getExpectedSID())
if (runtime.isCurrent && !runtime.isCurrent()) return superseded()
const responseData = isRecord(response.data) ? response.data : undefined
const code =
typeof responseData?.code === 'string' ? responseData.code : undefined
const bundle = runtime.parseBundle(responseData?.data)
if (responseData?.success === true && bundle) {
runtime.acceptBundle(bundle)
return { kind: 'authenticated', bundle }
}
if (response.status === 409 && code === 'AUTH_REFRESH_RACE') {
const delay = refreshRaceDelays[raceAttempt]
if (delay !== undefined) {
await runtime.wait(delay)
return run(raceAttempt + 1, allowMismatchRetry)
}
runtime.clear(false)
return { kind: 'out_of_sync', code }
}
if (response.status === 409 && code === 'AUTH_SESSION_MISMATCH') {
if (allowMismatchRetry) {
runtime.clear(false, 'idle')
return run(0, false)
}
runtime.clear(false)
return { kind: 'out_of_sync', code }
}
if (response.status === 401) {
runtime.clear(true)
return { kind: 'anonymous' }
}
if (!response.status || response.status >= 500 || response.status === 429) {
runtime.markTransient()
return {
kind: 'transient_error',
error: response.error ?? response.data,
}
}
runtime.clear(false)
return {
kind: 'out_of_sync',
code: code ?? 'AUTH_INVALID_REFRESH_RESPONSE',
}
}
return () => run(0, true)
}
async function requestRefresh(
expectedSID?: string
): Promise {
try {
const response = await authClient.post(
'/api/user/auth/refresh',
undefined,
{
headers: expectedSID ? { 'X-Auth-Session': expectedSID } : undefined,
}
)
return { status: response.status, data: response.data }
} catch (error: unknown) {
if (!axios.isAxiosError(error)) return { status: 0, error }
return {
status: error.response?.status ?? 0,
data: error.response?.data,
error,
}
}
}
function runRefresh(refreshEpoch: number): Promise {
return createRefreshRunner({
request: requestRefresh,
getExpectedSID: () => useAuthStore.getState().auth.session?.sid,
parseBundle: (value) => (isAuthBundle(value) ? value : null),
acceptBundle: (bundle) => applyAuthBundle(bundle, false),
clear: (synchronizeTabs, bootstrapState) => {
if (!synchronizeTabs && bootstrapState === 'idle') {
useAuthStore.getState().auth.reset('idle')
return
}
clearAuthentication(synchronizeTabs, bootstrapState)
},
markTransient: () => useAuthStore.getState().auth.setBootstrapState('idle'),
wait: waitForRefreshRace,
isCurrent: () => authEpoch === refreshEpoch,
})()
}
async function performRefreshWithBrowserLock(
refreshEpoch: number
): Promise {
try {
if (typeof navigator === 'undefined' || !navigator.locks) {
return runRefresh(refreshEpoch)
}
return navigator.locks.request(
'new-api:auth-refresh',
{ mode: 'exclusive' },
() => runRefresh(refreshEpoch)
)
} catch (error: unknown) {
useAuthStore.getState().auth.setBootstrapState('idle')
return { kind: 'transient_error', error }
}
}
export function refreshAuthentication(): Promise {
if (!refreshPromise) {
const refreshEpoch = authEpoch
refreshPromise = performRefreshWithBrowserLock(refreshEpoch).finally(() => {
refreshPromise = null
})
}
return refreshPromise
}
function currentValidAuthBundle(): AuthBundle | null {
const auth = useAuthStore.getState().auth
if (
!auth.user ||
!auth.accessToken ||
!auth.accessExpiresAt ||
!auth.session ||
auth.accessExpiresAt <= Math.floor(Date.now() / 1000)
) {
return null
}
return {
access_token: auth.accessToken,
token_type: 'Bearer',
access_expires_at: auth.accessExpiresAt,
user: auth.user,
session: auth.session,
}
}
export async function bootstrapAuthentication(): Promise {
const bundle = currentValidAuthBundle()
if (bundle) {
useAuthStore.getState().auth.setBootstrapState('complete')
return { kind: 'authenticated', bundle }
}
const auth = useAuthStore.getState().auth
const hasStaleSession = Boolean(auth.user && auth.session)
if (auth.bootstrapState === 'complete' && !hasStaleSession) {
return { kind: 'anonymous' }
}
auth.setBootstrapState('checking')
return refreshAuthentication()
}
export function getCommonHeaders(): Record {
const headers: Record = {
'Content-Type': 'application/json',
}
const accessToken = useAuthStore.getState().auth.accessToken
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
return headers
}
export async function getFreshAuthHeaders(): Promise> {
const auth = useAuthStore.getState().auth
const refreshBefore = Math.floor(Date.now() / 1000) + 60
if (
auth.accessToken &&
auth.accessExpiresAt &&
auth.accessExpiresAt > refreshBefore
) {
return getCommonHeaders()
}
const outcome = await refreshAuthentication()
if (outcome.kind === 'authenticated') {
return getCommonHeaders()
}
const current = useAuthStore.getState().auth
if (
current.accessToken &&
current.accessExpiresAt &&
current.accessExpiresAt > Math.floor(Date.now() / 1000)
) {
return getCommonHeaders()
}
if (outcome.kind === 'transient_error') {
throw new Error(t('Request failed'), { cause: outcome.error })
}
throw new Error(t('Session expired!'))
}