1/** 2 * TokenService 3 * 4 * Responsible for fetching and caching Cognigy Webchat authentication tokens. 5 * 6 * This service: 7 * - Fetches tokens from a backend identity endpoint. 8 * - Caches tokens until they expire. 9 * - Applies a configurable safety margin before expiry. 10 * - Treats `null` tokens as invalid for authentication purposes. 11 * - Gracefully handles network or server errors without throwing. 12 * 13 * Design Principles: 14 * - Minimal complexity. 15 * - Deterministic behavior. 16 * - Safe for use in Salesforce LWC environments. 17 * - No retry or concurrency control (intentional by design). 18 * 19 * Expected Server Response: 20 * { 21 * token: string | null, 22 * expiresAt: number (epoch milliseconds) 23 * } 24 * 25 * If `expiresAt` is missing or invalid, a fallback TTL is used. 26 */ 27export class TokenService { 28 29 /** 30 * Creates a new TokenService instance. 31 * 32 * @param {string} endpoint 33 * The HTTP endpoint used to retrieve the webchat token. 34 * 35 * @param {Object} [options] 36 * @param {number} [options.fallbackTtlMs=120000] 37 * Fallback time-to-live in milliseconds when the server does not 38 * provide a valid `expiresAt` value. 39 * 40 * @param {number} [options.safetyMarginMs=5000] 41 * Time in milliseconds subtracted from the server-provided expiry 42 * to avoid using tokens too close to expiration. 43 */ 44 constructor(endpoint, {fallbackTtlMs = 120_000, safetyMarginMs = 5_000} = {}) { 45 this.endpoint = endpoint; 46 this._fallbackTtlMs = fallbackTtlMs; 47 this._safetyMarginMs = safetyMarginMs; 48 49 /** 50 * @private 51 * @type {string|null|undefined} 52 * undefined â never fetched 53 * null â fetched but invalid/no token 54 * string â valid token value 55 */ 56 this._token = undefined; 57 58 /** 59 * @private 60 * @type {number} 61 * Epoch timestamp (ms) at which the cached token expires. 62 */ 63 this._expiresAt = 0; 64 } 65 66 /** 67 * Fetches a valid webchat token. 68 * 69 * If a cached token exists and is still valid, it is returned. 70 * Otherwise, a new token is requested from the backend. 71 * 72 * @param {boolean} [forceRefresh=false] 73 * If true, bypasses cache validation and forces a new fetch. 74 * 75 * @returns {Promise<string|null>} 76 * A valid token string, or `null` if: 77 * - The backend returned no token. 78 * - The request failed.
79 * - The token is considered invalid. 80 * 81 * This method never throws. 82 */ 83 async fetchWebchatToken(forceRefresh = false) { 84 if (!forceRefresh && this._isTokenValid()) { 85 return this._token; 86 } 87 88 try { 89 const res = await fetch(this.endpoint, { 90 method: 'POST', 91 headers: {'Content-Type': 'application/json'}, 92 credentials: 'include' 93 }); 94 95 return await this._handleTokenResponse(res); 96 97 } catch (e) { 98 console.warn('[TokenService] fetch failed', e); 99 this._cache(null, Date.now() + 10_000); 100 return null; 101 } 102 } 103 104 /** 105 * Handles the HTTP response from the token endpoint. 106 * 107 * @private 108 * @param {Response} res 109 * @returns {Promise<string|null>} 110 */ 111 async _handleTokenResponse(res) { 112 if (!res.ok) { 113 console.warn(`[TokenService] ${res.status} from token endpoint`); 114 this._cache(null, Date.now() + 10_000); 115 return null; 116 } 117 118 const data = await res.json(); 119 120 const token = data?.token ?? null; 121 const expiresAt = Number(data?.expiresAt); 122 123 const expiresAtMs = this._resolveExpiry(expiresAt); 124 125 this._cache(token, expiresAtMs); 126 127 return this._token; 128 } 129 130 /** 131 * Determines whether the currently cached token is valid. 132 * 133 * A token is valid if: 134 * - It is a non-empty string. 135 * - It has not yet expired. 136 * 137 * @private 138 * @returns {boolean} 139 */ 140 _isTokenValid() { 141 const now = Date.now(); 142 143 return ( 144 typeof this._token === 'string' && 145 this._token.length > 0 && 146 now < this._expiresAt 147 ); 148 } 149 150 /** 151 * Resolves the final expiration timestamp. 152 * 153 * Uses server-provided expiry when valid, 154 * otherwise falls back to configured TTL. 155 * 156 * @private 157 * @param {number} expiresAtFromServer 158 * @returns {number} Epoch timestamp in milliseconds. 159 */ 160 _resolveExpiry(expiresAtFromServer) { 161 const now = Date.now(); 162 163 if ( 164 Number.isFinite(expiresAtFromServer) && 165 expiresAtFromServer > now 166 ) { 167 return Math.max( 168 now + 1000, 169 expiresAtFromServer - this._safetyMarginMs 170 ); 171 } 172 173 return now + this._fallbackTtlMs; 174 } 175 176 /** 177 * Updates the internal cache. 178 * 179 * @private 180 * @param {string|null} token 181 * @param {number} expiresAtMs 182 */ 183 _cache(token, expiresAtMs) { 184 this._token = token ?? null; 185 this._expiresAt = expiresAtMs; 186 } 187}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.