export const ACCESS_TOKEN_KEY = 'cc-client-access-token' export const REFRESH_TOKEN_KEY = 'cc-client-refresh-token' export const REMEMBERED_LOGIN_ID_KEY = 'cc-client-remembered-login-id' export interface TokenPair { tokenType: string accessToken: string expiresIn: number refreshToken: string refreshExpiresIn: number } export function getAccessToken() { return localStorage.getItem(ACCESS_TOKEN_KEY) || sessionStorage.getItem(ACCESS_TOKEN_KEY) || '' } export function getRefreshToken() { return localStorage.getItem(REFRESH_TOKEN_KEY) || sessionStorage.getItem(REFRESH_TOKEN_KEY) || '' } // Only binds a retry to its original identity; never grants permissions. Expired // access tokens remain readable here because they are the normal refresh trigger. export function accessTokenIdentity(token: string): string | null { try { const encoded = token.split('.')[1] if (!encoded) return null const normalized = encoded.replace(/-/g, '+').replace(/_/g, '/') const bytes = Uint8Array.from(atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=')), character => character.charCodeAt(0)) const json = new TextDecoder().decode(bytes) // Match whole JSON strings first so digits inside arbitrary string values // are never rewritten. Preserve integer identity claims before JSON.parse // can round them; number/string representations then share one identity. const exactIdentityJson = json.replace(/"(?:\\.|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g, (part, offset: number) => { if (!/^-?\d+$/.test(part)) return part const key = json.slice(0, offset).match(/"(user_id|tenant_id|membership_id|operator_tenant_id)"\s*:\s*$/) return key ? JSON.stringify(part) : part }) const value = JSON.parse(exactIdentityJson) if (!value?.user_id || !value.tenant_id || !value.membership_id || value.app_code !== 'MERCHANT') return null return JSON.stringify([value.user_id, value.tenant_id, value.membership_id, value.operator_tenant_id, value.app_code, value.client_type].map(item => item == null ? null : String(item))) } catch { return null } } export function persistTokenPair(tokens: TokenPair) { // Replace the pair without publishing a transient logout to other tabs. sessionStorage.removeItem(ACCESS_TOKEN_KEY) sessionStorage.removeItem(REFRESH_TOKEN_KEY) sessionStorage.removeItem('cc-client-token') localStorage.removeItem('cc-client-token') localStorage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken) localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken) } export function getRememberedLoginId() { return localStorage.getItem(REMEMBERED_LOGIN_ID_KEY) || '' } export function persistRememberedLoginId(loginId: string, remember: boolean) { if (remember) localStorage.setItem(REMEMBERED_LOGIN_ID_KEY, loginId) else localStorage.removeItem(REMEMBERED_LOGIN_ID_KEY) } export function persistTemporaryAccessToken(accessToken: string) { clearSessionTokens() sessionStorage.setItem(ACCESS_TOKEN_KEY, accessToken) } export function clearSessionTokens() { for (const storage of [localStorage, sessionStorage]) { storage.removeItem(ACCESS_TOKEN_KEY) storage.removeItem(REFRESH_TOKEN_KEY) storage.removeItem('cc-client-token') } }