// Browser SPA auth helper. // // The server's jwtMiddleware handles transparent token refresh for cookie-based clients, // so the browser rarely needs to act on a 401. When it does arrive it means both tokens // are dead (refresh token expired or revoked) and the user must log in again. // // Usage: // import { apiFetch, setAuthRedirectUrl } from 'rokojori-auth-connector/browser/apiFetch'; // // // Optional — override the login URL if your app is on a different subdomain // setAuthRedirectUrl('https://account.rokojori.com/login.html'); // // const res = await apiFetch('/api/projects'); // const data = await res.json(); const AUTH_HOST = typeof window !== 'undefined' ? ( ( window as unknown as Record ).AUTH_HOST as string | undefined ) : undefined; let loginUrl = ( AUTH_HOST ?? 'https://account.rokojori.com' ) + '/login.html'; export function setAuthRedirectUrl( url: string ): void { loginUrl = url; } export async function apiFetch( input: RequestInfo | URL, init?: RequestInit ): Promise { const res = await fetch( input, init ); if ( res.status === 401 ) { const redirect = encodeURIComponent( window.location.href ); window.location.href = `${ loginUrl }?redirect=${ redirect }`; } return res; } // Decode the JWT access token payload without verifying the signature. // Use this to read userId, roles, and products from the token the server set. // The token is in the accessToken cookie — readable only if it is NOT HttpOnly, // which it is by default. In that case call GET /api/auth/me instead. export function decodeTokenPayload( token: string ): Record | null { try { const parts = token.split( '.' ); if ( parts.length !== 3 ) return null; const json = atob( parts[1].replace( /-/g, '+' ).replace( /_/g, '/' ) ); return JSON.parse( json ) as Record; } catch { return null; } }