npm package: @glideidentity/glide-fe-sdk-web
Description: Glide Phone Authentication SDK for web applications
Latest version: 2.2.0

# Glide Web Client SDK

The official web SDK for integrating Glide's carrier-grade phone verification into your web applications.

## Features

- 🚀 **Instant Verification** - Direct carrier verification without SMS delays
- 🔐 **Fraud Resistant** - Can't be intercepted or spoofed like SMS codes
- 📱 **Multi-Platform** - Desktop QR codes, iOS App Clips, Android deep links
- 🎨 **Customizable UI** - Built-in modal with themes and view modes
- 🔧 **Flexible Architecture** - Use the full SDK or just the core types
- 🌐 **Framework Support** - React, Vue, Angular, and vanilla JavaScript
- 📦 **Tree-Shakeable** - Import only what you need

## Installation

```bash
npm install @glideidentity/glide-fe-sdk-web
```

## Quick Start

### High-Level API (Recommended)

The simplest way to integrate phone verification:

```javascript
import { PhoneAuthClient, USE_CASE } from '@glideidentity/glide-fe-sdk-web';

const client = new PhoneAuthClient({
  endpoints: {
    prepare: '/api/magical-auth/prepare',
    reportInvocation: '/api/magical-auth/report-invocation',
    process: '/api/magical-auth/process',
  }
});

// Get a phone number
const result = await client.authenticate({
  use_case: USE_CASE.GET_PHONE_NUMBER
});
console.log('Phone:', result.phone_number);

// Or verify a specific number
const verified = await client.authenticate({
  use_case: USE_CASE.VERIFY_PHONE_NUMBER,
  phone_number: '+14155551234'
});
console.log('Verified:', verified.verified);
```

### Granular API

For more control over the authentication flow:

```javascript
import { PhoneAuthClient, USE_CASE } from '@glideidentity/glide-fe-sdk-web';

const client = new PhoneAuthClient({
  endpoints: {
    prepare: '/api/magical-auth/prepare',
    reportInvocation: '/api/magical-auth/report-invocation',
    process: '/api/magical-auth/process',
  }
});

// Step 1: Prepare the request
const prepared = await client.prepare({
  use_case: USE_CASE.VERIFY_PHONE_NUMBER,
  phone_number: '+14155551234'
});

// Step 2: Invoke authentication (shows UI based on strategy)
const invokeResult = await client.invokeSecurePrompt(prepared);
const credential = await invokeResult.credential;

// Step 3: Process the credential
const result = await client.verifyPhoneNumber(credential, invokeResult.session);

console.log('Verified:', result.verified);
```

## Configuration

### Client Options

```typescript
const client = new PhoneAuthClient({
  // Backend endpoints (with defaults shown)
  endpoints: {
    prepare: '/api/magical-auth/prepare',              // Prepare request endpoint
    process: '/api/magical-auth/process',              // Process credential endpoint
    reportInvocation: '/api/magical-auth/report-invocation', // ASR tracking endpoint
    polling: undefined,                              // Optional: Status polling endpoint
  },
  
  // Polling configuration
  pollingInterval: 2000,      // Polling interval in ms (default: 2000)
  maxPollingAttempts: 30,     // Max attempts before timeout (default: 30)
  timeout: 30000,             // API request timeout in ms (default: 30000)
  
  // Debug options
  debug: false,               // Enable console logging (default: false)
  devtools: {
    showMobileConsole: false  // Show on-screen console on mobile (default: false)
  },
  
  // Dev environment routing (e.g., 'dev7' adds 'developer' header)
  devEnv: undefined,
});
```

### Invoke Options

Customize the authentication UI and behavior:

```typescript
const result = await client.invokeSecurePrompt(prepared, {
  // Prevent SDK from showing any UI (use your own)
  preventDefaultUI: false,
  
  // Polling configuration (overrides client config)
  pollingInterval: 2000,
  maxPollingAttempts: 30,
  
  // Modal customization (for desktop QR strategy)
  modalOptions: {
    theme: 'auto',              // 'dark' | 'light' | 'auto'
    viewMode: 'toggle',         // 'toggle' | 'dual' | 'pre-step'
    title: 'Scan to Verify',    // Custom title text
    description: '',            // Optional subtitle
    showCloseButton: true,      // Show X button
    closeOnBackdropClick: true, // Close on outside click
    closeOnEscape: true,        // Close on Escape key
  }
});
```

## Modal Customization

The SDK provides a built-in modal for desktop QR code display.

### View Modes

| Mode | Description | Best For |
|------|-------------|----------|
| `toggle` | Single QR with iOS/Android switch | Clean, minimal UI (default) |
| `dual` | Side-by-side QR codes | Users who may not know their OS |
| `pre-step` | OS selection screen first | Guided experience |

### Themes

| Theme | Description |
|-------|-------------|
| `auto` | Follows system preference (default) |
| `dark` | Dark background with light text |
| `light` | Light background with dark text |

### Custom UI

To use your own UI instead of the built-in modal:

```javascript
const result = await client.invokeSecurePrompt(prepared, {
  preventDefaultUI: true
});

// For desktop strategy, display your own QR code
if (result.strategy === 'desktop') {
  // Access QR data from the prepare response
  const qrData = prepared.data;
  displayYourQRCode(qrData.ios_qr_image, qrData.android_qr_image);
  
  // Wait for credential
  const credential = await result.credential;
  
  // Or cancel if needed
  result.cancel?.();
}
```

## Authentication Strategies

The SDK automatically selects the best strategy based on the device:

| Strategy | Platform | Description |
|----------|----------|-------------|
| `ts43` | Android Chrome 128+ | Native Digital Credentials API |
| `link` | iOS / Android | App Clip (iOS) or carrier app (Android) |
| `desktop` | Desktop browsers | QR code scanned by mobile |

### Strategy-Specific Handling

```javascript
const result = await client.invokeSecurePrompt(prepared);

switch (result.strategy) {
  case 'ts43':
    // Android: Credential returned directly
    const credential = await result.credential;
    break;
    
  case 'link':
    // iOS: Opens App Clip, polls for completion
    // Android: Opens carrier's privileged app, polls for completion
    const credential = await result.credential;
    // Optionally cancel polling
    result.cancel?.();
    break;
    
  case 'desktop':
    // Desktop: Shows QR modal, polls for completion
    const credential = await result.credential;
    result.cancel?.();
    break;
}
```

## Framework Integration

### React

```jsx
import { usePhoneAuth, USE_CASE } from '@glideidentity/glide-fe-sdk-web/react';

function PhoneVerification() {
  const { authenticate, isLoading, error, result } = usePhoneAuth({
    endpoints: {
      prepare: '/api/magical-auth/prepare',
      reportInvocation: '/api/magical-auth/report-invocation',
      process: '/api/magical-auth/process',
    }
  });

  const handleVerify = async () => {
    try {
      const result = await authenticate({
        use_case: USE_CASE.VERIFY_PHONE_NUMBER,
        phone_number: '+14155551234'
      });
      console.log('Verified:', result.verified);
    } catch (err) {
      console.error('Failed:', err);
    }
  };

  return (
    <button onClick={handleVerify} disabled={isLoading}>
      {isLoading ? 'Verifying...' : 'Verify Phone'}
    </button>
  );
}
```

### Vue

```vue
<script setup>
import { usePhoneAuth, USE_CASE } from '@glideidentity/glide-fe-sdk-web/vue';

const { authenticate, isLoading, error, result } = usePhoneAuth({
  endpoints: {
    prepare: '/api/magical-auth/prepare',
    reportInvocation: '/api/magical-auth/report-invocation',
    process: '/api/magical-auth/process',
  }
});

const handleVerify = async () => {
  await authenticate({
    use_case: USE_CASE.VERIFY_PHONE_NUMBER,
    phone_number: '+14155551234'
  });
};
</script>

<template>
  <button @click="handleVerify" :disabled="isLoading">
    {{ isLoading ? 'Verifying...' : 'Verify Phone' }}
  </button>
</template>
```

### Vanilla JavaScript (Browser)

```html
<script src="https://unpkg.com/@glideidentity/glide-fe-sdk-web/dist/browser/web-client-sdk.min.js"></script>
<script>
  const { PhoneAuthClient, USE_CASE } = GlideWebClientSDK;
  
  const client = new PhoneAuthClient({
    endpoints: {
      prepare: '/api/magical-auth/prepare',
      reportInvocation: '/api/magical-auth/report-invocation',
      process: '/api/magical-auth/process',
    }
  });
  
  document.getElementById('verify-btn').onclick = async () => {
    const result = await client.authenticate({
      use_case: USE_CASE.VERIFY_PHONE_NUMBER,
      phone_number: '+14155551234'
    });
    console.log('Verified:', result.verified);
  };
</script>
```

## Advanced Features

### Custom HTTP Headers

Add custom headers to all SDK requests:

```javascript
const client = new PhoneAuthClient({
  endpoints: { ... },
  headers: {
    common: {
      'X-Custom-Header': 'your-value'
    }
  }
});
```

### Custom Logger

Provide your own logger implementation:

```javascript
const client = new PhoneAuthClient({
  endpoints: { ... },
  logger: {
    debug: (msg, data) => console.debug('[SDK]', msg, data),
    info: (msg, data) => console.info('[SDK]', msg, data),
    warn: (msg, data) => console.warn('[SDK]', msg, data),
    error: (msg, data) => console.error('[SDK]', msg, data),
  }
});
```

### Custom HTTP Client

Bring your own HTTP client for full control:

```javascript
const client = new PhoneAuthClient({
  endpoints: { ... },
  httpClient: {
    post: async (url, body, options) => {
      const response = await yourHttpLib.post(url, body, options);
      return response.data;
    },
    get: async (url, options) => {
      const response = await yourHttpLib.get(url, options);
      return response.data;
    }
  }
});
```

## Core Package

For advanced use cases, you can import just the types and validators without the full client:

```typescript
import { 
  // Types
  type PrepareRequest,
  type PrepareResponse,
  type InvokeResult,
  type SessionInfo,
  
  // Constants
  USE_CASE,
  AUTHENTICATION_STRATEGY,
  ERROR_CODES,
  
  // Validators
  validatePhoneNumber,
  validatePlmn,
  
  // Type Guards
  isTS43Strategy,
  isLinkStrategy,
  isDesktopStrategy,
  isAuthError,
} from '@glideidentity/glide-fe-sdk-web/core';

// Use validators
const { valid, error } = validatePhoneNumber('+14155551234');

// Use type guards
if (isDesktopStrategy(result)) {
  // TypeScript knows result has desktop-specific properties
}
```

## Error Handling

```javascript
import { ERROR_CODES, isAuthError } from '@glideidentity/glide-fe-sdk-web';

try {
  await client.authenticate({ ... });
} catch (error) {
  if (isAuthError(error)) {
    switch (error.code) {
      case ERROR_CODES.USER_CANCELLED:
        // User closed the modal or cancelled
        break;
      case ERROR_CODES.TIMEOUT:
        // Authentication timed out
        break;
      case ERROR_CODES.NETWORK_ERROR:
        // Network request failed
        break;
      default:
        // Handle other errors
        console.error(error.message);
    }
  }
}
```

## Type Reference

### Use Cases

```typescript
type UseCase = 'GetPhoneNumber' | 'VerifyPhoneNumber';

// Or use the constant
USE_CASE.GET_PHONE_NUMBER    // 'GetPhoneNumber'
USE_CASE.VERIFY_PHONE_NUMBER // 'VerifyPhoneNumber'
```

### Prepare Request

```typescript
interface PrepareRequest {
  use_case: UseCase;
  phone_number?: string;      // Required for VerifyPhoneNumber
  parent_session_id?: string; // For cross-device flows
}
```

### Invoke Result

```typescript
interface InvokeResult {
  strategy: 'ts43' | 'link' | 'desktop';
  session: SessionInfo;
  credential: Promise<string>;  // Resolves to credential token
  cancel?: () => void;          // Available for link and desktop
  invocationReport: Promise<{ success: boolean; error?: string }>;
}
```

## ASR Tracking (reportInvocation)

The SDK automatically tracks authentication attempts for Authentication Success Rate (ASR) metrics. This is handled via the `reportInvocation` endpoint.

### Behavior

| Aspect | Description |
|--------|-------------|
| **Fire-and-forget** | Never blocks the main authentication flow |
| **Non-critical** | Auth succeeds even if reporting fails |
| **Observable** | Developers CAN check if it worked (optional) |
| **No latency impact** | User doesn't wait for this API call |

### Configuration

The `reportInvocation` endpoint is included in the default config:

```typescript
const client = new PhoneAuthClient({
  endpoints: {
    prepare: '/api/magical-auth/prepare',
    process: '/api/magical-auth/process',
    reportInvocation: '/api/magical-auth/report-invocation',  // ASR tracking (default)
  }
});
```

### Monitoring (Optional)

If you want to monitor whether ASR tracking succeeded:

```typescript
const result = await client.invokeSecurePrompt(prepared);

// Main flow continues immediately
const credential = await result.credential;

// Optional: Check report status (doesn't block)
result.invocationReport?.then(({ success, error }) => {
  if (!success) {
    console.warn('ASR tracking failed:', error);
    // Optionally send to your own analytics
  }
});
```

### Backend Endpoint

Your backend should implement a simple pass-through endpoint:

```typescript
// POST /api/magical-auth/report-invocation
app.post('/api/magical-auth/report-invocation', async (req, res) => {
  const { session_id } = req.body;
  
  try {
    const result = await glide.magicalAuth.reportInvocation({ session_id });
    res.json({ success: result.success });
  } catch (error) {
    // Always return HTTP 200 - never fail the auth flow
    res.json({ success: false, error: error.message });
  }
});
```

### Responses

```typescript
interface GetPhoneNumberResponse {
  phone_number: string;
  aud?: string;               // Audience from carrier
  sim_swap?: SimSwapInfo;     // SIM swap detection info
  device_swap?: DeviceSwapInfo; // Device swap (IMEI change) detection info
}

interface VerifyPhoneNumberResponse {
  phone_number: string;
  verified: boolean;
  aud?: string;               // Audience from carrier
  sim_swap?: SimSwapInfo;     // SIM swap detection info
  device_swap?: DeviceSwapInfo; // Device swap (IMEI change) detection info
}
```

## SIM Swap Detection

The SDK returns SIM swap detection information when available from carriers. This helps identify potential fraud.

### Response Fields

```typescript
interface SimSwapInfo {
  /** Whether the SIM swap check completed successfully */
  checked: boolean;
  /** Risk level based on SIM change recency */
  risk_level?: 'RISK_LEVEL_HIGH' | 'RISK_LEVEL_MEDIUM' | 'RISK_LEVEL_LOW' | 'RISK_LEVEL_UNKNOWN';
  /** Human-readable time since last SIM change (e.g., "0-4 hours", "more than 3 years") */
  age_band?: string;
  /** When the check was performed (RFC3339) */
  checked_at?: string;
  /** Reason for failure if checked=false */
  reason?: 'timeout' | 'carrier_not_supported' | 'disabled' | 'error';
}
```

### Risk Levels

| Level | Description | Recommended Action |
|-------|-------------|-------------------|
| `RISK_LEVEL_HIGH` | SIM changed within 7 days | Block or require additional verification |
| `RISK_LEVEL_MEDIUM` | SIM changed 7-30 days ago | Consider step-up authentication |
| `RISK_LEVEL_LOW` | SIM changed 30+ days ago | Normal processing |
| `RISK_LEVEL_UNKNOWN` | Could not determine | Use default policy |

### Helper Functions

```typescript
import { 
  isHighRiskSimSwap,
  isMediumOrHighRiskSimSwap,
  wasSimSwapChecked,
  getMinDaysSinceSimSwap,
} from '@glideidentity/glide-fe-sdk-web';

const result = await client.getPhoneNumber(credential, session);

// Check if SIM swap was checked
if (wasSimSwapChecked(result.sim_swap)) {
  // Check risk level
  if (isHighRiskSimSwap(result.sim_swap)) {
    // High risk - recent SIM change
    requireAdditionalVerification();
  } else if (isMediumOrHighRiskSimSwap(result.sim_swap)) {
    // Medium risk - consider step-up auth
    showWarning();
  }
  
  // Get minimum days since SIM change
  const minDays = getMinDaysSinceSimSwap(result.sim_swap);
  console.log(`SIM unchanged for at least ${minDays} days`);
}
```

## Device Swap Detection

The SDK returns device swap (IMEI change) detection information when available from carriers. This is a parallel, independent signal alongside SIM swap detection. Not all carriers support this signal — the `device_swap` field is only present when the carrier provides it.

### Response Fields

```typescript
interface DeviceSwapInfo {
  /** Whether the device swap check completed successfully */
  checked: boolean;
  /** Risk level based on device change recency */
  risk_level?: 'RISK_LEVEL_HIGH' | 'RISK_LEVEL_MEDIUM' | 'RISK_LEVEL_LOW' | 'RISK_LEVEL_UNKNOWN';
  /** Human-readable time since last device (IMEI) change (e.g., "0-4 hours", "more than 3 years") */
  age_band?: string;
  /** When the check was performed (RFC3339) */
  checked_at?: string;
  /** Reason for failure if checked=false */
  reason?: 'timeout' | 'carrier_not_supported' | 'disabled' | 'error';
}
```

### Risk Levels

| Level | Description | Recommended Action |
|-------|-------------|-------------------|
| `RISK_LEVEL_HIGH` | Device changed within 7 days | Block or require additional verification |
| `RISK_LEVEL_MEDIUM` | Device changed 7-30 days ago | Consider step-up authentication |
| `RISK_LEVEL_LOW` | Device changed 30+ days ago | Normal processing |
| `RISK_LEVEL_UNKNOWN` | Could not determine | Use default policy |

### Helper Functions

```typescript
import { 
  isHighRiskDeviceSwap,
  isMediumOrHighRiskDeviceSwap,
  wasDeviceSwapChecked,
  getMinDaysSinceDeviceSwap,
} from '@glideidentity/glide-fe-sdk-web';

const result = await client.getPhoneNumber(credential, session);

// Check if device swap was checked
if (wasDeviceSwapChecked(result)) {
  // Check risk level
  if (isHighRiskDeviceSwap(result)) {
    // High risk - recent device change
    requireAdditionalVerification();
  } else if (isMediumOrHighRiskDeviceSwap(result)) {
    // Medium risk - consider step-up auth
    showWarning();
  }
  
  // Get minimum days since device change
  const minDays = getMinDaysSinceDeviceSwap(result);
  console.log(`Device unchanged for at least ${minDays} days`);
}
```

## Browser Support

| Browser | Strategy | Requirements |
|---------|----------|--------------|
| Chrome Android 128+ | TS43 | Digital Credentials API |
| Safari iOS | Link | App Clips support |
| Chrome/Edge/Firefox Desktop | Desktop | Any modern version |

## Support

- [Documentation](https://docs.glideidentity.com)
- [API Reference](https://docs.glideidentity.com/api-reference)

## License

MIT