npm package: @ketrics/sdk-frontend
Description: Ketrics SDK for tenant application frontends
Author: Ketrics
Latest version: 0.3.0

# 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:

1. **Decoupling authentication**: Tenant apps don't need to understand the parent's auth mechanism
2. **Lazy token delivery**: Tokens are pushed to tenant apps only when needed via postMessage
3. **Proactive refresh**: SDK automatically monitors expiration and requests refresh before tokens become invalid
4. **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 interval
```

#### Token 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 token
```

### Business Rules Implemented

**Token Expiration Checks**:
- `isTokenExpiringSoon(bufferSeconds)`: Returns true if expiry - now ≤ bufferSeconds * 1000 milliseconds
- `isTokenExpired()`: 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**:
- `AuthConfig` object with optional `refreshBuffer` (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**:
```typescript
// 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()` returns `null`, downstream code must check
- Invalid timestamp format in localStorage: `parseInt()` with radix 10 prevents octal interpretation
- Missing KetricsContext values: Each getter returns `null` independently, 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 before `initAutoRefresh()`: No-op, safe operation
- `clearTokens()` 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             # Documentation
```

**File Responsibilities**:

- **index.ts** (43 lines): Public module API. Exports `AuthManager` class, `createAuthManager()` factory, type definitions (`AuthConfig`, `KetricsContext`), and constants (`STORAGE_KEYS`, `MESSAGE_TYPES`). Single entry point for all consumers.

- **auth.ts** (255 lines): `AuthManager` class 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. Private `handleMessage()` method implements TOKEN_UPDATE protocol.

- **types.ts** (59 lines): TypeScript interface definitions and constant exports. `AuthConfig` interface defines refresh buffer and two optional callbacks. `KetricsContext` interface describes complete authentication/execution context. `STORAGE_KEYS` const object maps all localStorage keys. `MESSAGE_TYPES` const object defines postMessage event types. Enables type-safe integration with parent window.

### Key Functions/Classes and Purposes

#### AuthManager Class

**Constructor/Lifecycle**:
```typescript
// 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 from `ketrics_access_token` localStorage key or null
- `getAccessTokenExpiry()`: Returns Unix millisecond timestamp from `ketrics_access_token_expiry` key, parsed as base-10 integer
- `isTokenExpiringSoon(bufferSeconds = 60)`: Boolean check: `expiry - Date.now() <= bufferSeconds * 1000`
- `isTokenExpired()`: Boolean check: `Date.now() >= expiry`

**Context Access Methods**:
- `getContext()`: Returns `KetricsContext` object aggregating all stored values; always succeeds with nulls where values missing
- `getTenantId()`, `getUserId()`, `getApplicationId()`, `getRuntimeApiUrl()`: Individual localStorage key accessors

**Refresh Coordination**:
- `requestRefresh()`: Sends `{ type: 'REQUEST_TOKEN_REFRESH' }` via `window.parent.postMessage(..., '*')`. Logs success, catches and reports errors via `onRefreshError` callback
- `initAutoRefresh(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 initialized`
- `stopAutoRefresh()`: Clears interval, removes event listener, nullifies references. Safe to call multiple times
- `clearTokens()`: Stops auto-refresh, then removes all `ketrics_*` keys from localStorage. Final cleanup on logout

**Internal Message Handler**:
- `handleMessage(event)`: Checks for `TOKEN_UPDATE` message type. If contains `accessToken`, stores both token and expiry to localStorage. Invokes `onTokenUpdated` callback if registered. All errors caught and logged

**Factory Function**:
```typescript
export function createAuthManager(): AuthManager {
  return new AuthManager();
}
```

#### Type Definitions

**AuthConfig**:
```typescript
interface AuthConfig {
  refreshBuffer?: number;
  onTokenUpdated?: (accessToken: string) => void;
  onRefreshError?: (error: Error) => void;
}
```

**KetricsContext**:
```typescript
interface KetricsContext {
  accessToken: string | null;
  accessTokenExpiry: number | null;
  tenantId: string | null;
  userId: string | null;
  applicationId: string | null;
  runtimeApiUrl: string | null;
}
```

**Constants**:
```typescript
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**:
```typescript
window.parent.postMessage(
  { type: 'REQUEST_TOKEN_REFRESH' },
  '*'  // Wildcard origin
)
```

**Inbound Messages to SDK**:
```typescript
{
  type: 'TOKEN_UPDATE',
  accessToken: 'eyJhbGc...',
  accessTokenExpiry: 1738794600000
}
```

**Expectations**:
- Parent window listens for `REQUEST_TOKEN_REFRESH` messages
- Parent validates request origin is iframe (SDK doesn't; uses wildcard)
- Parent calls backend to refresh token
- Parent sends `TOKEN_UPDATE` with 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**:
```typescript
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_TOKEN` message from parent window
- Stores all context values in localStorage under `ketrics_*` keys
- Navigates iframe to tenant application URL
- Sets `TOKEN_RECEIVED_AT` timestamp (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 object
```

### Token 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`, `messageHandler`
- `handleMessage()` modifies: localStorage only (external)
- `stopAutoRefresh()` clears: `checkTimer`, `messageHandler`
- `clearTokens()` removes: all localStorage `ketrics_*` 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 cleared
```

Error 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

```bash
npm install @ketrics/sdk-frontend
```

Requires Node.js 24.0.0 or higher. Compatible with any frontend framework (React, Vue, Angular, vanilla JS).

### Basic Setup

**Minimal Example**:
```typescript
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**:
```typescript
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**:
```typescript
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**:
```typescript
// 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)**:
```typescript
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**:
```typescript
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):

1. **Token Access Methods**: Mock localStorage, test getters return values or null
   ```typescript
   test('getAccessToken returns token from localStorage', () => {
     localStorage.setItem('ketrics_access_token', 'test-token');
     const auth = new AuthManager();
     expect(auth.getAccessToken()).toBe('test-token');
   });
   ```

2. **Expiration Checks**: Mock Date.now(), test boundary conditions
   ```typescript
   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
   });
   ```

3. **postMessage Handling**: Mock window.parent.postMessage, listen for calls
   ```typescript
   test('requestRefresh sends postMessage', () => {
     const spy = jest.spyOn(window.parent, 'postMessage');
     const auth = new AuthManager();
     auth.requestRefresh();
     expect(spy).toHaveBeenCalledWith(
       { type: 'REQUEST_TOKEN_REFRESH' },
       '*'
     );
   });
   ```

4. **Message Events**: Send synthetic MessageEvent, verify localStorage updates
   ```typescript
   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');
   });
   ```

5. **Auto-Refresh Lifecycle**: Verify timers started/stopped, event listeners added/removed
   ```typescript
   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):

1. Simulate CDN loader populating localStorage
2. Load tenant app and initialize SDK
3. Verify auth manager reads context correctly
4. Simulate parent postMessage events
5. 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_REFRESH` sent to parent
- [ ] Verify `onTokenUpdated` callback invoked when parent sends TOKEN_UPDATE
- [ ] Test `clearTokens()` removes all `ketrics_*` 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**:
```bash
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 clean
```

**Build Output**:
- `dist/index.js` - Compiled module entry point
- `dist/auth.js` - Compiled AuthManager class
- `dist/types.js` - Compiled type definitions
- `dist/index.d.ts` - TypeScript declarations (public API)
- `dist/auth.d.ts` - AuthManager type definitions
- `dist/types.d.ts` - Type definitions
- `.map` files - 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**:
```bash
# 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 major
```

**Manual Publishing**:
```bash
# 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 version
```

**Automated Publishing** (via GitHub Actions):
1. Workflow file: `publish-sdk-frontend.yml`
2. Trigger: Manual dispatch via GitHub Actions UI (`workflow_dispatch`)
3. 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 build` locally and verify no errors
- [ ] Run tests if available
- [ ] Verify package.json `files` array includes dist/
- [ ] Verify `.npmignore` excludes 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.