@ketrics/sdk-frontend
v0.3.1
Published
Ketrics SDK for tenant application frontends
Downloads
78
Maintainers
Readme
Ketrics Frontend SDK
TypeScript SDK for tenant application frontends running within the Ketrics platform. Provides authentication token management, context retrieval, and secure communication with the parent Ketrics application.
Overview
Purpose
The @ketrics/sdk-frontend package enables tenant applications deployed as iframes within the Ketrics platform to manage authentication tokens and retrieve execution context. Its core responsibility is bridging the authentication gap between sandboxed tenant applications and the parent Ketrics window, which holds the actual authentication state.
Key responsibilities:
- Token Access: Read JWT access tokens stored in localStorage by the CDN loader bootstrap script
- Context Retrieval: Provide tenant ID, user ID, application ID, and Data Plane API URL context
- Token Lifecycle Management: Monitor token expiration and trigger refresh requests when tokens approach expiry
- Parent Communication: Establish secure postMessage communication channel with the parent Ketrics window for token refresh events
Architectural Position
The SDK operates within a multi-layer iframe architecture:
┌─────────────────────────────────────────────────────────────────┐
│ Ketrics Frontend (LaunchPage.tsx) │
│ Manages auth state, refreshes tokens, controls iframe lifecycle │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ iframe │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ CDN loader.html (bootstrap) │ │ │
│ │ │ └─ Receives AUTH_TOKEN from parent │ │ │
│ │ │ └─ Stores context in localStorage │ │ │
│ │ │ └─ Redirects to tenant app │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Tenant Application (uses @ketrics/sdk-frontend) │ │ │
│ │ │ ├─ reads context from localStorage │ │ │
│ │ │ ├─ makes API calls with retrieved token │ │ │
│ │ │ ├─ requests token refresh via postMessage │ │ │
│ │ │ └─ receives token updates via postMessage │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘The SDK specifically handles the "Tenant Application" layer, maintaining no direct coupling to the parent window beyond postMessage events.
Boundaries
The SDK does NOT handle:
- Initial authentication or login flows
- Token issuance or validation
- Storage persistence beyond browser localStorage
- CORS or API gateway configuration
Business Logic
Problem Solved
Tenant applications are deployed as iframes with sandboxed origins from a CDN, preventing direct access to the parent window's authentication state. However, these applications require authenticated access to the Data Plane API on behalf of the logged-in user. This SDK solves this by:
- Decoupling authentication: Tenant apps don't need to understand the parent's auth mechanism
- Lazy token delivery: Tokens are pushed to tenant apps only when needed via postMessage
- Proactive refresh: SDK automatically monitors expiration and requests refresh before tokens become invalid
- Event-driven updates: Token updates from parent are received asynchronously via postMessage
Core Workflows
Initialization Workflow
1. User launches tenant app from Ketrics UI
2. LaunchPage loads iframe with CDN loader.html?target=<app_url>
3. Loader sends LOADER_READY → parent window
4. Parent sends AUTH_TOKEN with full context → loader
5. Loader stores context in localStorage using ketrics_* keys
6. Loader navigates iframe to tenant application URL
7. Tenant app initializes SDK: auth = createAuthManager()
8. Tenant app starts auto-refresh: auth.initAutoRefresh(config)
9. SDK reads context from localStorage, sets up message listener and 10s expiration check intervalToken Refresh Workflow
1. SDK's 10-second check detects token expiring within buffer window (default 60s)
2. SDK sends REQUEST_TOKEN_REFRESH → parent window via postMessage
3. Parent receives message, validates request origin is iframe, calls refreshAuth()
4. Parent backend issues new token with updated expiry
5. Parent sends TOKEN_UPDATE → iframe with new token and expiry
6. SDK receives TOKEN_UPDATE, validates message source, stores in localStorage
7. SDK invokes onTokenUpdated callback with new token
8. Tenant app updates HTTP client headers with fresh tokenBusiness Rules Implemented
Token Expiration Checks:
isTokenExpiringSoon(bufferSeconds): Returns true if expiry - now ≤ bufferSeconds * 1000 millisecondsisTokenExpired(): Returns true if now ≥ expiry timestamp- Default buffer is 60 seconds; prevents requests with tokens that would expire mid-request
Auto-Refresh Mechanism:
- Interval-based checking every 10 seconds (not event-driven) for robustness
- Immediate check on
initAutoRefresh()call to catch already-expired tokens - Fire-and-forget requests; parent window handles retry via next interval check
- Only one auto-refresh cycle active at a time; calling
initAutoRefresh()twice stops first cycle
Storage Isolation:
- All operations wrapped in try-catch; missing/unavailable localStorage returns null
- Each localStorage key is namespaced with
ketrics_prefix to avoid conflicts - TOKEN_RECEIVED_AT key reserved for future analytics but not currently used
Input/Output Expectations
Input to SDK:
AuthConfigobject with optionalrefreshBuffer(seconds, default 60) and two callbacks- Message events from parent window containing
{ type, accessToken, accessTokenExpiry }payload - localStorage key-value pairs populated by CDN loader
Output from SDK:
- Return values: string (tokens), number (timestamp), boolean (expiry checks), KetricsContext object
- Side effects: localStorage writes, postMessage events to parent, console logs
- Callbacks invoked:
onTokenUpdated(string)when parent pushes new token,onRefreshError(Error)when postMessage fails
Data Contract Example:
// Input: TOKEN_UPDATE from parent
{
type: 'TOKEN_UPDATE',
accessToken: 'eyJhbGciOiJIUzI1NiIs...',
accessTokenExpiry: 1738794600000 // Unix ms
}
// Output: KetricsContext to tenant app
{
accessToken: 'eyJhbGciOiJIUzI1NiIs...',
accessTokenExpiry: 1738794600000,
tenantId: 'tenant-uuid',
userId: 'user-uuid',
applicationId: 'app-uuid',
runtimeApiUrl: 'https://api.ketrics.io/v1'
}Edge Cases Handled
Missing/Invalid Data:
- Missing access token:
getAccessToken()returnsnull, downstream code must check - Invalid timestamp format in localStorage:
parseInt()with radix 10 prevents octal interpretation - Missing KetricsContext values: Each getter returns
nullindependently, not entire context
Storage Failures:
- localStorage disabled/unavailable: All getters/setters wrapped in try-catch, fail gracefully
- localStorage quota exceeded:
setItem()failures logged but don't break token update flow
Lifecycle Issues:
- Multiple
initAutoRefresh()calls: Previous interval and event listener properly cleaned up stopAutoRefresh()called beforeinitAutoRefresh(): No-op, safe operationclearTokens()called during active auto-refresh: Stops refresh first, then clears
Communication Issues:
- Parent window unreachable (cross-origin blocked):
postMessage()fails silently in most browsers, error caught and logged - Message from wrong origin: postMessage uses wildcard '*' origin (consider security implications in deployment)
- TOKEN_UPDATE with missing accessToken field: Update skipped, old token remains valid
Technical Details
Technology Stack and Dependencies
Language & Compilation:
- TypeScript 5.x (target: ES2020, module: ES2020)
- Compiles to standard ES modules with CommonJS interoperability
- Full type safety with strict mode enabled
Runtime Environment:
- Target browsers: ES2020 support (Chrome 80+, Firefox 74+, Safari 14+, Edge 80+)
- Requires DOM APIs:
localStorage,window.parent.postMessage(),setInterval/clearInterval,addEventListener/removeEventListener - Node.js 24.0.0+ for build/development
Build & Distribution:
- TypeScript compiler (tsc) for build
- npm for package management
- Published to npm registry as public scoped package
@ketrics/sdk-frontend - Distributed as ES modules with TypeScript declaration files
Zero Runtime Dependencies: The SDK has no npm dependencies, only devDependencies (TypeScript). This minimizes bundle size and security surface.
File Structure
ketrics-sdk-frontend/
├── src/
│ ├── index.ts # Package entry point, public API exports
│ ├── auth.ts # AuthManager class, core token management logic
│ └── types.ts # TypeScript interfaces, constants, storage key definitions
├── dist/ # Compiled JavaScript and declaration files (build output)
├── node_modules/ # Development dependencies
├── package.json # Package manifest, npm scripts, metadata
├── tsconfig.json # TypeScript compiler configuration
├── .npmignore # Files excluded from npm package (src, dist source maps, tests)
└── README.md # DocumentationFile Responsibilities:
index.ts (43 lines): Public module API. Exports
AuthManagerclass,createAuthManager()factory, type definitions (AuthConfig,KetricsContext), and constants (STORAGE_KEYS,MESSAGE_TYPES). Single entry point for all consumers.auth.ts (255 lines):
AuthManagerclass implementing token lifecycle management. Private state:checkTimer(interval ID),config(AuthConfig),messageHandler(bound event listener). Public methods for token access, expiration checking, refresh coordination, and auto-refresh lifecycle. PrivatehandleMessage()method implements TOKEN_UPDATE protocol.types.ts (59 lines): TypeScript interface definitions and constant exports.
AuthConfiginterface defines refresh buffer and two optional callbacks.KetricsContextinterface describes complete authentication/execution context.STORAGE_KEYSconst object maps all localStorage keys.MESSAGE_TYPESconst object defines postMessage event types. Enables type-safe integration with parent window.
Key Functions/Classes and Purposes
AuthManager Class
Constructor/Lifecycle:
// Private state initialized
private checkTimer: ReturnType<typeof setInterval> | null = null;
private config: AuthConfig = {};
private messageHandler: ((event: MessageEvent) => void) | null = null;Token Access Methods:
getAccessToken(): Returns current JWT fromketrics_access_tokenlocalStorage key or nullgetAccessTokenExpiry(): Returns Unix millisecond timestamp fromketrics_access_token_expirykey, parsed as base-10 integerisTokenExpiringSoon(bufferSeconds = 60): Boolean check:expiry - Date.now() <= bufferSeconds * 1000isTokenExpired(): Boolean check:Date.now() >= expiry
Context Access Methods:
getContext(): ReturnsKetricsContextobject aggregating all stored values; always succeeds with nulls where values missinggetTenantId(),getUserId(),getApplicationId(),getRuntimeApiUrl(): Individual localStorage key accessors
Refresh Coordination:
requestRefresh(): Sends{ type: 'REQUEST_TOKEN_REFRESH' }viawindow.parent.postMessage(..., '*'). Logs success, catches and reports errors viaonRefreshErrorcallbackinitAutoRefresh(config): Orchestrates complete auto-refresh setup. Stops existing refresh first, stores config, binds message handler, starts 10s interval loop, checks immediately. Logs[Ketrics Auth] Auto-refresh initializedstopAutoRefresh(): Clears interval, removes event listener, nullifies references. Safe to call multiple timesclearTokens(): Stops auto-refresh, then removes allketrics_*keys from localStorage. Final cleanup on logout
Internal Message Handler:
handleMessage(event): Checks forTOKEN_UPDATEmessage type. If containsaccessToken, stores both token and expiry to localStorage. InvokesonTokenUpdatedcallback if registered. All errors caught and logged
Factory Function:
export function createAuthManager(): AuthManager {
return new AuthManager();
}Type Definitions
AuthConfig:
interface AuthConfig {
refreshBuffer?: number;
onTokenUpdated?: (accessToken: string) => void;
onRefreshError?: (error: Error) => void;
}KetricsContext:
interface KetricsContext {
accessToken: string | null;
accessTokenExpiry: number | null;
tenantId: string | null;
userId: string | null;
applicationId: string | null;
runtimeApiUrl: string | null;
}Constants:
STORAGE_KEYS = {
ACCESS_TOKEN: 'ketrics_access_token',
ACCESS_TOKEN_EXPIRY: 'ketrics_access_token_expiry',
TENANT_ID: 'ketrics_tenant_id',
USER_ID: 'ketrics_user_id',
APPLICATION_ID: 'ketrics_application_id',
RUNTIME_API_URL: 'ketrics_runtime_api_url',
TOKEN_RECEIVED_AT: 'ketrics_token_received_at',
}
MESSAGE_TYPES = {
REQUEST_TOKEN_REFRESH: 'REQUEST_TOKEN_REFRESH',
TOKEN_UPDATE: 'TOKEN_UPDATE',
}Configuration Options and Environment Variables
AuthConfig.refreshBuffer:
- Type:
number(seconds) - Default: 60 seconds
- Controls how early before token expiry to request refresh
- Set higher (e.g., 300) for APIs with slow response times; set lower for minimal token lifetime
- Applied to all expiration checks once configured
AuthConfig.onTokenUpdated:
- Type:
(accessToken: string) => void - Invoked whenever parent sends TOKEN_UPDATE with valid accessToken
- Recommended use: Update HTTP client default headers (e.g.,
axios.defaults.headers.common['Authorization']) - Executes synchronously after localStorage write; errors don't prevent operation
AuthConfig.onRefreshError:
- Type:
(error: Error) => void - Invoked if postMessage fails (rare; typically cross-origin issues)
- Errors are pre-converted to Error objects
- Recommended use: Log to monitoring/analytics system
No Environment Variables: SDK reads no environment variables. All configuration is runtime API. localStorage keys are hardcoded constants.
External Integrations
Parent Window (Ketrics Frontend)
Protocol: Cross-origin postMessage
Outbound Messages from SDK:
window.parent.postMessage(
{ type: 'REQUEST_TOKEN_REFRESH' },
'*' // Wildcard origin
)Inbound Messages to SDK:
{
type: 'TOKEN_UPDATE',
accessToken: 'eyJhbGc...',
accessTokenExpiry: 1738794600000
}Expectations:
- Parent window listens for
REQUEST_TOKEN_REFRESHmessages - Parent validates request origin is iframe (SDK doesn't; uses wildcard)
- Parent calls backend to refresh token
- Parent sends
TOKEN_UPDATEwith fresh token - Parent sends updates proactively when token changes outside SDK request (optional but recommended)
Data Plane API
Integration Point: Tenant application uses runtimeApiUrl from context
Usage Pattern:
const { runtimeApiUrl, tenantId, applicationId } = auth.getContext();
const token = auth.getAccessToken();
fetch(`${runtimeApiUrl}/tenants/${tenantId}/applications/${applicationId}/functions/getData`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: JSON.stringify(...)
})API Expectations:
- Base URL in
runtimeApiUrl(e.g.,https://api.ketrics.io/v1) - Accepts Bearer token authentication
- Endpoints follow pattern:
/tenants/{id}/applications/{id}/functions/{name} - Returns 401 Unauthorized if token invalid/expired
CDN Loader (loader.html)
Upstream Integration: SDK reads data populated by loader
Loader Responsibilities:
- Receives
AUTH_TOKENmessage from parent window - Stores all context values in localStorage under
ketrics_*keys - Navigates iframe to tenant application URL
- Sets
TOKEN_RECEIVED_ATtimestamp (currently unused by SDK)
Contract: SDK assumes all ketrics_* keys already populated when createAuthManager() called. No validation of key format or values.
Data Flow
Initialization Sequence
1. CDN loader writes to localStorage:
ketrics_access_token = "eyJ..."
ketrics_access_token_expiry = "1738794600000"
ketrics_tenant_id = "tenant-uuid"
ketrics_user_id = "user-uuid"
ketrics_application_id = "app-uuid"
ketrics_runtime_api_url = "https://api.ketrics.io/v1"
2. Tenant app imports SDK:
import { createAuthManager } from '@ketrics/sdk-frontend'
3. Tenant app instantiates:
const auth = createAuthManager()
// AuthManager object created, private state initialized
4. Tenant app configures auto-refresh:
auth.initAutoRefresh({
refreshBuffer: 60,
onTokenUpdated: (token) => {
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
}
})
// Message listener attached
// 10s interval started
// Immediate expiration check performed
// If expired/expiring, REQUEST_TOKEN_REFRESH sent
5. Tenant app reads context:
const { runtimeApiUrl, tenantId } = auth.getContext()
// Reads from localStorage, returns aggregated objectToken Access Flow
Tenant App AuthManager localStorage
│ │ │
│ getAccessToken() │ │
│ ─────────────────────────────> │
│ │ getItem('ketrics_...') │
│ │ ──────────────────────────>
│ │ <──────────────────────────
│ │ 'eyJ...' (string) │
│ <───────────────────────────── │
│ token: 'eyJ...' │ │
Token used in API request:
│ fetch(url, { │ │
│ headers: { │ │
│ 'Authorization': `Bearer ${token}` │
│ } │ │
│ }) │ │Token Refresh Flow
Time AuthManager Parent Window localStorage
│
│ (10s interval check)
├─ isTokenExpiringSoon() → true (expiry < now + 60s)
│
├─ postMessage({ type: 'REQUEST_TOKEN_REFRESH' })
│──────────────────────────────────>
│ (parent receives message)
│ refreshAuth() called
│ backend issues new token
│ <── new token received
│
│ <──────────────────────────
│ postMessage({ type: 'TOKEN_UPDATE', accessToken: '...', accessTokenExpiry: ... })
│
├─ handleMessage() invoked
│
├─ setItem('ketrics_access_token', newToken)
├─ setItem('ketrics_access_token_expiry', newExpiry) ──────> localStorage updated
│
├─ onTokenUpdated(newToken) callback invoked
│ (tenant app updates axios headers, etc.)
│
└─ (next interval check at current + 10s)Data Transformations
Token Expiry Format:
- Stored: Unix timestamp in milliseconds (number converted to string in localStorage)
- Retrieved:
parseInt(storedValue, 10)back to number - Compared:
expiry - Date.now()(both milliseconds, comparison valid) - Transmitted: Parent sends as number in TOKEN_UPDATE payload; SDK stores as string
No Cryptographic Operations: Tokens are opaque strings; SDK performs no validation, decoding, or signature verification. Parent window and backend responsible for token security.
No Transformations on Context: KetricsContext values returned as-is from localStorage; no type coercion or normalization applied.
State Mutations
AuthManager Private State Changes:
initAutoRefresh()sets:checkTimer,config,messageHandlerhandleMessage()modifies: localStorage only (external)stopAutoRefresh()clears:checkTimer,messageHandlerclearTokens()removes: all localStorageketrics_*keys
External State Changes:
- localStorage writes: 2 keys per TOKEN_UPDATE (token + expiry)
- Message events: 1 REQUEST_TOKEN_REFRESH per refresh cycle
- Event listeners: 1 message handler registered/removed per init/stop cycle
Error Handling
Error Scenarios and Handling
| Scenario | Handler | Outcome | |----------|---------|---------| | localStorage disabled/unavailable | try-catch in all getters/setters | Returns null / operation silent fails | | postMessage call fails | try-catch in requestRefresh() | Error logged, onRefreshError callback invoked | | TOKEN_UPDATE missing accessToken field | Guard check in handleMessage() | Update skipped, old token remains | | Invalid expiry format in localStorage | parseInt() with radix 10 | Returns NaN, isTokenExpiringSoon() treats as expired | | Multiple initAutoRefresh() calls | stopAutoRefresh() called first | Previous timers/listeners cleaned up | | Parent window unreachable | postMessage() fails silently (browser security) | Error caught and logged, callback invoked on next check attempt | | Message origin not validated | Uses wildcard '*' | Accepts messages from any origin (vulnerability if strict validation needed) |
Retry Logic and Fallback Mechanisms
Auto-Refresh Retry Strategy:
- Not event-driven; uses 10-second polling interval
- If parent doesn't respond to REQUEST_TOKEN_REFRESH, next interval (10s later) will check again
- No exponential backoff or maximum retry limit
- SDK assumes parent window always available (or will recover)
Token Expiration Fallback:
- If token already expired when app starts:
initAutoRefresh()detects and immediately requests refresh - If refresh takes > 60s (default buffer): tenant app might make requests with expired token; parent/API returns 401
- Tenant app responsible for handling 401 responses (retry with fresh token or redirect to login)
Missing Context Fallback:
- If localStorage keys not populated by loader: getters return null
- Tenant app must check for null before using context values
- SDK provides no alternative fallback (no hardcoded defaults)
Logging Approach
Console Logging: All logs prefixed with [Ketrics Auth] for easy filtering in browser DevTools
Log Levels and Messages:
Info level (console.log):
[Ketrics Auth] Auto-refresh initialized
[Ketrics Auth] Requested token refresh from parent
[Ketrics Auth] Token updated from parent
[Ketrics Auth] Auto-refresh stopped
[Ketrics Auth] Tokens clearedError level (console.error):
[Ketrics Auth] Failed to request token refresh: {error message}
[Ketrics Auth] Failed to store updated token: {error message}No Logging of Sensitive Data: Logs don't include actual tokens, keys, or user IDs (security-conscious)
Log Accessibility: Console logs only appear in browser developer tools; no server-side logging by SDK
Application Responsibility: Tenant app should wrap SDK callbacks to implement custom logging/monitoring
Usage
Installation
npm install @ketrics/sdk-frontendRequires Node.js 24.0.0 or higher. Compatible with any frontend framework (React, Vue, Angular, vanilla JS).
Basic Setup
Minimal Example:
import { createAuthManager } from '@ketrics/sdk-frontend';
const auth = createAuthManager();
auth.initAutoRefresh();
// Token available immediately (if loader populated localStorage)
const token = auth.getAccessToken();Recommended Setup with Callbacks:
import axios from 'axios';
import { createAuthManager } from '@ketrics/sdk-frontend';
const auth = createAuthManager();
auth.initAutoRefresh({
refreshBuffer: 60, // Request refresh 60 seconds before expiry
onTokenUpdated: (newToken) => {
// Update HTTP client headers with fresh token
axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
console.log('Token refreshed and client updated');
},
onRefreshError: (error) => {
// Log refresh failures to monitoring system
console.error('Token refresh failed:', error);
// Optionally trigger user re-authentication
},
});
// Create axios instance with initial token
const token = auth.getAccessToken();
if (token) {
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
export { auth, axios };Example Invocations
Reading Context and Making API Calls:
async function fetchUserData() {
const context = auth.getContext();
// Check context available
if (!context.runtimeApiUrl || !context.tenantId) {
throw new Error('Authentication context not initialized');
}
const token = auth.getAccessToken();
if (!token) {
throw new Error('No access token available');
}
const response = await fetch(
`${context.runtimeApiUrl}/tenants/${context.tenantId}/data`,
{
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
}
);
if (response.status === 401) {
// Token expired or invalid; wait for auto-refresh or clear and re-authenticate
auth.clearTokens();
throw new Error('Token invalid, please refresh');
}
return response.json();
}Checking Token Expiration:
// Check if refresh is needed soon (within 2 minutes)
if (auth.isTokenExpiringSoon(120)) {
console.warn('Token will expire soon; manual refresh recommended');
}
// Check if token already expired
if (auth.isTokenExpired()) {
console.error('Token has expired; clearing and requiring re-login');
auth.clearTokens();
window.location.reload();
}
// Get exact expiry timestamp for UI display
const expiry = auth.getAccessTokenExpiry();
if (expiry) {
const expiresAt = new Date(expiry);
console.log(`Token expires at: ${expiresAt.toISOString()}`);
}Component Lifecycle Integration (React Example):
import { useEffect, useRef } from 'react';
import { createAuthManager } from '@ketrics/sdk-frontend';
function MyComponent() {
const authRef = useRef(null);
useEffect(() => {
// Initialize on mount
authRef.current = createAuthManager();
authRef.current.initAutoRefresh({
refreshBuffer: 60,
onTokenUpdated: (token) => {
// Update HTTP client or state
console.log('Token updated');
},
});
// Cleanup on unmount
return () => {
authRef.current?.stopAutoRefresh();
};
}, []);
return <div>{/* component content */}</div>;
}Cleanup and Logout:
function handleLogout() {
// Stop auto-refresh and clear all tokens
auth.clearTokens();
// Redirect to login or parent window
window.parent.postMessage({ type: 'LOGOUT' }, '*');
// or redirect
// window.location.href = '/login';
}Testing Approach
Unit Testing (recommended tools: Jest, Vitest with jsdom):
Token Access Methods: Mock localStorage, test getters return values or null
test('getAccessToken returns token from localStorage', () => { localStorage.setItem('ketrics_access_token', 'test-token'); const auth = new AuthManager(); expect(auth.getAccessToken()).toBe('test-token'); });Expiration Checks: Mock Date.now(), test boundary conditions
test('isTokenExpiringSoon with buffer', () => { const now = Date.now(); const expiryIn30s = now + 30 * 1000; localStorage.setItem('ketrics_access_token_expiry', expiryIn30s.toString()); const auth = new AuthManager(); expect(auth.isTokenExpiringSoon(60)).toBe(true); // Expires < 60s away expect(auth.isTokenExpiringSoon(20)).toBe(false); // Expires > 20s away });postMessage Handling: Mock window.parent.postMessage, listen for calls
test('requestRefresh sends postMessage', () => { const spy = jest.spyOn(window.parent, 'postMessage'); const auth = new AuthManager(); auth.requestRefresh(); expect(spy).toHaveBeenCalledWith( { type: 'REQUEST_TOKEN_REFRESH' }, '*' ); });Message Events: Send synthetic MessageEvent, verify localStorage updates
test('TOKEN_UPDATE updates localStorage', () => { const auth = new AuthManager(); auth.initAutoRefresh(); const event = new MessageEvent('message', { data: { type: 'TOKEN_UPDATE', accessToken: 'new-token', accessTokenExpiry: 9999999999999, }, }); window.dispatchEvent(event); expect(localStorage.getItem('ketrics_access_token')).toBe('new-token'); });Auto-Refresh Lifecycle: Verify timers started/stopped, event listeners added/removed
test('stopAutoRefresh clears timer and removes listener', () => { const auth = new AuthManager(); auth.initAutoRefresh(); const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); auth.stopAutoRefresh(); expect(clearIntervalSpy).toHaveBeenCalled(); expect(removeEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); });
Integration Testing (test with loader simulation):
- Simulate CDN loader populating localStorage
- Load tenant app and initialize SDK
- Verify auth manager reads context correctly
- Simulate parent postMessage events
- Verify SDK responds with refresh requests and callback invocations
Manual Testing Checklist:
- [ ] Launch app, verify initial token loaded from localStorage
- [ ] Check browser console for
[Ketrics Auth] Auto-refresh initialized - [ ] Wait past refresh buffer, verify
REQUEST_TOKEN_REFRESHsent to parent - [ ] Verify
onTokenUpdatedcallback invoked when parent sends TOKEN_UPDATE - [ ] Test
clearTokens()removes allketrics_*keys - [ ] Test
stopAutoRefresh()stops polling (no more logs every 10s) - [ ] Test with localStorage disabled (private browsing); verify graceful null returns
Development and Build
Development Workflow:
cd ketrics-sdk-frontend
# Install dependencies
npm install
# Build TypeScript to dist/
npm run build
# Watch mode for development
npm run dev
# Clean build artifacts
npm run cleanBuild Output:
dist/index.js- Compiled module entry pointdist/auth.js- Compiled AuthManager classdist/types.js- Compiled type definitionsdist/index.d.ts- TypeScript declarations (public API)dist/auth.d.ts- AuthManager type definitionsdist/types.d.ts- Type definitions.mapfiles - Source maps for debugging
TypeScript Configuration:
- Target: ES2020 (modern browser support)
- Module: ES2020 (native ES modules)
- Strict: true (type safety)
- Declaration: true (generate .d.ts files)
- Source maps enabled for debugging
Publishing
Version Management:
# Bump patch version (0.2.0 → 0.2.1)
npm version patch
# Bump minor version (0.2.0 → 0.3.0)
npm version minor
# Bump major version (0.2.0 → 1.0.0)
npm version majorManual Publishing:
# Authenticate with npm
npm login
# Build and publish to npm registry
npm run build
npm publish --access public
# Verify published
npm view @ketrics/sdk-frontend versionAutomated Publishing (via GitHub Actions):
- Workflow file:
publish-sdk-frontend.yml - Trigger: Manual dispatch via GitHub Actions UI (
workflow_dispatch) - Steps:
- Checkout repository
- Install dependencies
- Run TypeScript build (validates code)
- Publish to npm registry with
--access public
Pre-publish Checklist:
- [ ] Increment version in package.json
- [ ] Update CHANGELOG (if maintained)
- [ ] Run
npm run buildlocally and verify no errors - [ ] Run tests if available
- [ ] Verify package.json
filesarray includes dist/ - [ ] Verify
.npmignoreexcludes src/ and node_modules - [ ] Commit and push changes
- [ ] Create git tag matching version
- [ ] Trigger publish workflow or run
npm publish
Summary
The Ketrics Frontend SDK is a minimal, focused authentication token management library for tenant applications deployed in iframes within the Ketrics platform. It provides zero-dependency token access, expiration monitoring, and parent-window communication via a clean TypeScript API. The SDK assumes the parent window and CDN loader handle initial token delivery and refresh orchestration, making it a thin integration layer rather than a complete authentication system. All operations are localStorage-backed and postMessage-coordinated, ensuring compatibility with iframe security boundaries.
