npm package: @jakobcooldown/react-csr-sdk
Description: Mockery SDK for dynamic bundle loading in web applications
Author: Mockery
Latest version: 1.0.1

# Mockery React CSR SDK

Dynamic bundle loading for full React app replacement based on user/tenant context.

## Overview

The Mockery React CSR SDK enables you to **completely replace** your React application bundle based on who is logged in.

## How It Works

1. **HTML loads** → **Bundle loader checks localStorage** → **Loads custom OR default bundle** → **Single React app starts**
2. No React component conflicts or DOM manipulation issues
3. True full-app replacement, not overlay customizations

## Security Architecture

The SDK follows a secure server-to-server architecture:

```
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   Browser   │    │   Vendor    │    │   Mockery   │
│    (SDK)    │    │  Backend    │    │     API     │
└─────────────┘    └─────────────┘    └─────────────┘
       │                  │                  │
       │ GET /api/bundle  │                  │
       ├─────────────────►│                  │
       │                  │ GET /vendor-api  │
       │                  ├─────────────────►│
       │                  │ (with API key)   │
       │                  │                  │
       │                  │   bundle URL     │
       │                  │◄─────────────────┤
       │   bundle URL     │                  │
       │◄─────────────────┤                  │
       │                  │                  │
    SDK loads bundle      │                  │
```

- **Browser**: Never sees API keys, calls vendor backend
- **Vendor Backend**: Authenticates users, calls Mockery API server-to-server
- **Mockery API**: Returns bundle URLs based on tenant configuration

## Installation

```bash
npm install @mockery/react-csr-sdk
```

## Quick Start

### 1. Include the Bundle Loader

Add the bundle loader script to your HTML **before any React bundles**:

```html
<!DOCTYPE html>
<html>
<head>
  <!-- Include Mockery bundle loader FIRST -->
  <script src="node_modules/@mockery/react-csr-sdk/dist/bundle-loader.js"></script>
  
  <!-- Mark your default bundle -->
  <script data-mockery-default-bundle type="module" src="./assets/index-abc123.js"></script>
</head>
<body>
  <div id="root"></div>
</body>
</html>
```

### 2. Create Vendor Backend Endpoint

Your backend should authenticate users and call the Mockery API:

```javascript
// GET /api/user/bundle
app.get('/api/user/bundle', authenticateUser, async (req, res) => {
  const user = req.user; // Your authenticated user
  
  if (!user.tenantId) {
    return res.json({ bundleUrl: undefined });
  }
  
  try {
    // Call Mockery API server-to-server with your API key
    const response = await fetch(
      `${MOCKERY_API_URL}/vendor-api/products/${PRODUCT_SLUG}/tenants/${user.tenantId}/bundle`,
      {
        headers: {
          'X-API-Key': process.env.MOCKERY_API_KEY, // Keep API keys on server
          'Content-Type': 'application/json',
        },
      }
    );
    
    if (response.ok) {
      const data = await response.json();
      res.json({ bundleUrl: data.bundle_url });
    } else {
      res.json({ bundleUrl: undefined });
    }
  } catch (error) {
    console.error('Failed to fetch bundle from Mockery:', error);
    res.json({ bundleUrl: undefined });
  }
});
```

### 3. Use SDK in Frontend

```javascript
import { setCustomBundle, clearCustomBundle } from '@mockery/react-csr-sdk';

// Handle user login - call your own backend
async function handleUserLogin(credentials) {
  try {
    // 1. Authenticate with your backend (your existing auth flow)
    const authResponse = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(credentials),
      credentials: 'include'
    });
    
    if (authResponse.ok) {
      // 2. Get bundle URL from your backend
      const bundleResponse = await fetch('/api/user/bundle', {
        credentials: 'include'
      });
      
      if (bundleResponse.ok) {
        const { bundleUrl } = await bundleResponse.json();
        
        // setCustomBundle will exit early if bundleUrl is unchanged
        setCustomBundle(bundleUrl); // Page reloads only if bundle changed
      }
    }
  } catch (error) {
    console.error('Login failed:', error);
  }
}

// Clear custom bundle on logout
async function handleUserLogout() {
  try {
    await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
    clearCustomBundle(); // Reloads with default bundle
  } catch (error) {
    console.error('Logout failed:', error);
  }
}
```

## API Reference

### Bundle Management

#### `setCustomBundle(bundleUrl, reload?)`

Sets a custom bundle URL and optionally reloads the page. Exits early (no reload) if the bundle URL is unchanged.

```javascript
setCustomBundle('https://cdn.example.com/custom-bundle.js', true);
setCustomBundle(undefined); // Clear bundle
```

**Parameters:**
- `bundleUrl: string | undefined` - Bundle URL or undefined to clear
- `reload: boolean` - Whether to reload page (default: true)

#### `getCustomBundle()`

Gets the currently set custom bundle URL.

```javascript
const currentBundle = getCustomBundle();
// Returns: string | undefined
```

#### `clearCustomBundle(reload?)`

Clears the custom bundle and optionally reloads to use the default.

```javascript
clearCustomBundle(true);
```

#### `reloadWithBundle(bundleUrl)`

Sets a bundle URL and immediately reloads the page.

```javascript
reloadWithBundle('https://cdn.example.com/new-bundle.js');
reloadWithBundle(undefined); // Clear and reload
```

### Utility Functions

#### `setDebugMode(enabled)`

Enable/disable debug logging.

```javascript
setDebugMode(true); // See bundle loading logs
```

## Integration Patterns

### User Login Flow

```javascript
async function handleUserLogin(credentials) {
  // 1. Authenticate with your backend
  const user = await authenticateUser(credentials);
  
  // 2. Get bundle URL from your backend
  if (user) {
    try {
      const response = await fetch('/api/user/bundle', {
        credentials: 'include'
      });
      
      if (response.ok) {
        const { bundleUrl } = await response.json();
        
        // Only reloads if bundle URL changed
        setCustomBundle(bundleUrl);
      }
    } catch (error) {
      console.error('Failed to load custom bundle:', error);
      // App continues with default bundle
    }
  }
}
```

### User Logout Flow

```javascript
async function handleUserLogout() {
  // 1. Clear session with your backend
  await logoutUser();
  
  // 2. Clear custom bundle and reload with default
  clearCustomBundle();
}
```

### Tenant Switching

```javascript
async function switchTenant(newTenantId) {
  // 1. Update tenant in your backend
  await updateUserTenant(newTenantId);
  
  // 2. Get new tenant's bundle URL
  const response = await fetch('/api/user/bundle', {
    credentials: 'include'
  });
  
  if (response.ok) {
    const { bundleUrl } = await response.json();
    setCustomBundle(bundleUrl); // Page reloads only if bundle changed
  }
}
```

### Error Handling

```javascript
async function loadUserBundle() {
  try {
    const response = await fetch('/api/user/bundle', {
      credentials: 'include'
    });
    
    if (response.ok) {
      const { bundleUrl } = await response.json();
      setCustomBundle(bundleUrl);
    }
  } catch (error) {
    console.error('Bundle loading failed:', error);
    
    // Optional: Show user notification
    showNotification('Custom features unavailable, using default app');
    
    // App continues with default bundle - no action needed
  }
}
```

## Global API (No Module Import Required)

If you can't use ES modules, the bundle loader exposes global functions:

```javascript
// Available on window.mockery after bundle-loader.js loads
window.mockery.setCustomBundle(url);
window.mockery.getCustomBundle();
window.mockery.clearCustomBundle();
window.mockery.reloadWithBundle(url);
window.mockery.setDebug(enabled);
```

## How Bundle Replacement Works

1. **Bundle Loader Script**: Runs before any React code
2. **localStorage Check**: Looks for `mockery_bundle_url` 
3. **Bundle Decision**: 
   - If custom URL found → Prevents default bundle, loads custom bundle
   - If no custom URL → Allows default bundle to load normally
4. **Single App**: Only one React application ever starts

## Bundle Requirements

Your custom bundles should:

- Be complete, standalone React applications
- Include all dependencies (React, ReactDOM, etc.)
- Mount to the same DOM element (`#root`)
- Handle their own routing and state management

## Backend Integration Requirements

Your backend endpoint (`/api/user/bundle` or similar) should:

1. **Authenticate the user** (session, JWT, etc.)
2. **Determine user's tenant ID** from your user database
3. **Call Mockery API server-to-server** with your API key
4. **Return bundle URL** to frontend (or undefined if no custom bundle)

Example response format:
```json
{
  "bundleUrl": "https://cdn.example.com/tenant-123/bundle.js"
}
```

Or if no custom bundle:
```json
{
  "bundleUrl": undefined
}
```

## Security Benefits

- **API Keys Protected**: Never exposed to browser
- **User Authentication**: Your backend validates identity  
- **Authorization**: Your backend ensures user can access their tenant
- **Audit Trail**: Your backend can log bundle requests
- **Rate Limiting**: Your backend can implement limits

## Debugging

Enable debug mode to see bundle loading logs:

```javascript
import { setDebugMode } from '@mockery/react-csr-sdk';
setDebugMode(true);

// Or globally:
window.mockery.setDebug(true);
```

This will log:
- Bundle URL resolution
- Bundle loading attempts
- Fallback scenarios
- Error details

## TypeScript Support

The SDK includes full TypeScript definitions:

```typescript
import type { MockeryConfig } from '@mockery/react-csr-sdk';
```

## Examples

See the `examples/` directory for complete implementation examples.

## Support

For issues or questions, please see the Mockery documentation or contact support.