@glideidentity/glide-fe-sdk-web
v3.0.0
Published
Glide Phone Authentication SDK for web applications
Readme
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
- Strategy-driven — One API across
ts43(Android Digital Credentials),link(mobile-web App Clip / native App Link), anddesktop(cross-device QR); Glide's Magical Auth service selects the strategy server-side from the carrier + your user agent and returns it inprepare - Customizable UI — Built-in modal with themes (single universal QR on desktop)
- Flexible Architecture — Use the full SDK or just the core types
- Framework Support — React, Vue, and vanilla JavaScript
- Tree-Shakeable — Import only what you need
- SIM-swap & device-swap signals — first-party carrier risk metadata exposed alongside the verified phone number
Installation
npm install @glideidentity/glide-fe-sdk-webQuick Start
Two integration shapes ship with the SDK. The Granular API is what we recommend for production — it lets you call
prepare()early so the carrier prompt opens with no perceptible delay when the user clicks the CTA. The High-Level API (authenticate()) bundles all three steps into one call and is the fastest way to see the SDK working end-to-end the first time you wire it up.
Recommended: Granular API
prepare → invokeSecurePrompt → verifyPhoneNumber (or getPhoneNumber) as three separate calls. The win is eager prepare: kick the network round-trip off as soon as you have enough context (e.g. when the user types their phone number, when the input field gains focus, or right after step navigation lands the user on the verification screen). By the time the user actually taps the verify button, the prepare response is already cached locally — invokeSecurePrompt() opens the carrier UI immediately, with no spinner.
The browser's user-gesture security model means invokeSecurePrompt() itself must still run from a synchronous click handler. Eager prepare doesn't change that — it just removes the network wait that would otherwise sit between the click and the carrier prompt.
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: Call prepare() AS EARLY AS YOU HAVE THE PHONE NUMBER.
// e.g. on input blur, on form-step transition, or right after the user types it.
// This pre-pays the prepare round-trip so the carrier prompt opens instantly later.
const prepared = await client.prepare({
use_case: USE_CASE.VERIFY_PHONE_NUMBER,
phone_number: '+14155551234',
});
// Step 2: From a click handler — must be a real user gesture.
verifyBtn.addEventListener('click', async () => {
const invokeResult = await client.invokeSecurePrompt(prepared); // opens carrier UI immediately
const credential = await invokeResult.credential;
// Step 3: Hand the credential to your backend (which calls Glide's verify-phone-number / get-phone-number).
const result = await client.verifyPhoneNumber(credential, invokeResult.session);
console.log('Verified:', result.verified);
});High-Level API (quickest path to "it works")
authenticate() runs prepare → invokeSecurePrompt → verifyPhoneNumber (or getPhoneNumber) for you in a single call. Easiest to integrate the first time and great for demos / spikes — the trade-off is that the carrier prompt won't open until the prepare round-trip completes inside the click handler, so on slower networks the user sees a brief delay between tap and carrier UI.
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',
}
});
verifyBtn.addEventListener('click', async () => {
// Get a phone number (use_case omitted for VERIFY_PHONE_NUMBER + a phone_number)
const result = await client.authenticate({
use_case: USE_CASE.GET_PHONE_NUMBER
});
console.log('Phone:', result.phone_number);
});Configuration
Client Options
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 (your backend long-polls Magical Auth)
reportInvocation: '/api/magical-auth/report-invocation', // ASR tracking endpoint
eligibility: '/api/magical-auth/eligibility', // Eligibility check endpoint (optional)
},
timeout: 30000, // API request timeout in ms (default: 30000)
sessionTimeout: 300000, // Max wait for desktop session completion — QR scan → mobile auth → backend `/process` chain (default: 5 min)
// Strategy override — pin a specific strategy instead of letting the server auto-select.
// Must be one of the strategies returned by checkEligibility() for the target phone number.
// If not available, prepare() returns a STRATEGY_NOT_AVAILABLE error.
authenticationStrategy: 'ts43', // Optional: 'ts43' | 'link' | 'desktop' (default: auto-selected by server)
// Debug options
debug: false, // Enable console logging (default: false)
devtools: {
showMobileConsole: false // Show on-screen console on mobile (default: false)
},
});Invoke Options
Customize the authentication UI and behavior:
const result = await client.invokeSecurePrompt(prepared, {
// Prevent SDK from showing any UI (use your own)
preventDefaultUI: false,
// Modal customization (for desktop QR strategy)
modalOptions: {
theme: 'auto', // 'dark' | 'light' | 'auto'
title: 'Scan to Verify', // Custom title text
description: '', // Optional subtitle shown below the QR
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. It renders a single universal QR code — the Mobile Auth companion app detects the device platform at runtime, so there is no OS toggle / picker on desktop.
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:
import { getDesktopQRImage } from '@glideidentity/glide-fe-sdk-web';
const result = await client.invokeSecurePrompt(prepared, {
preventDefaultUI: true
});
if (result.strategy === 'desktop') {
const qrImage = getDesktopQRImage(prepared.data); // base64 PNG data URI
const url = prepared.data.data.url; // universal URL inside the QR
displayYourQRCode(qrImage, url);
const credential = await result.credential;
// result.cancel?.() to abort the flow
}Authentication Strategies
Glide's Magical Auth service selects the strategy server-side based on the carrier (looked up from the phone number / PLMN) and your user agent (platform, browser, native vs mobile-web), then returns it in prepare as authentication_strategy. Branch your code on the returned value, not on navigator.userAgent or browser sniffing — Glide has already factored in the OS / browser, and the same OS may receive different strategies for different carriers.
| Strategy | Where it runs | Mechanism |
|----------|---------------|-----------|
| ts43 | Android Chrome 128+ | Native Digital Credentials API hand-off via the platform Credential Manager. |
| link | Any mobile browser whose carrier uses an App Clip / App Link redirect-with-code handoff | The Web SDK runs the mobile-web mode of link: opens the carrier App Clip in a new tab, the result returns via a completion-page redirect, device binding uses an HttpOnly cookie (fe_code) plus a URL fragment (agg_code), and the original tab observes a localStorage signal to retrieve the result. (The native return-channel mode of link — Universal Link / App Link with Secure Enclave–backed binding — is handled by the React Native and Android SDKs, not this one.) |
| desktop | Desktop browsers | QR code scanned by a Glide-aware mobile companion. |
Strategy-Specific Handling
const result = await client.invokeSecurePrompt(prepared);
switch (result.strategy) {
case 'ts43':
// Android: Credential returned directly
const credential = await result.credential;
break;
case 'link':
// Mobile-web mode of `link`: SDK opens the carrier App Clip in a new tab,
// listens for the completion-page `localStorage` signal, then resolves the credential.
const credential = await result.credential;
// Optionally cancel the in-flight flow
result.cancel?.();
break;
case 'desktop':
// Desktop: QR modal; credential resolves when mobile completes auth (backend `/process` long-poll)
const credential = await result.credential;
result.cancel?.();
break;
}Framework Integration
React
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
<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)
<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>Eligibility & Strategy Selection
Check Eligibility
Query available authentication strategies before starting a flow. The response is relative to the user's device: available_platforms names the phone the user needs, and requirements.target is self (this device) or companion (a separate phone, e.g. desktop QR/CTAP).
client_info.user_agent is required by the API; the web SDK auto-fills it from navigator.userAgent when you don't pass client_info, so a plain call works in the browser. Pass client_info explicitly to test a specific device.
const eligibility = await client.checkEligibility({
phone_number: '+14155551234',
// client_info optional in the browser — auto-filled from navigator.userAgent
});
// Example (desktop Chrome, T-Mobile):
// eligibility.available_options = [
// { authentication_strategy: 'ts43', name: 'SIM-Based Authentication', available_platforms: ['android'],
// requirements: { target: 'companion', android_min_version: 14, android_min_sdk: 34 } },
// { authentication_strategy: 'desktop', name: 'Mobile Companion Authentication', available_platforms: ['android'],
// requirements: { target: 'companion', android_min_version: 14, android_min_sdk: 34 } },
// ]Strategy Override
Pass authentication_strategy in the prepare request to explicitly select a strategy from the eligible set:
const prepared = await client.prepare({
use_case: USE_CASE.VERIFY_PHONE_NUMBER,
phone_number: '+14155551234',
authentication_strategy: 'link', // override server-assigned strategy
});If the requested strategy is not available, the API returns a STRATEGY_NOT_AVAILABLE error.
Advanced Features
Bring Your Own HTTP Client
The default HTTP client is intentionally minimal — it only handles JSON
serialization, timeouts, abort signals, and error parsing. Anything else
(custom headers, auth tokens, environment routing, retries, axios/ky/fetch
wrappers, request/response interceptors) belongs in your own httpClient
implementation:
const client = new PhoneAuthClient({
endpoints: { ... },
httpClient: {
post: async (url, body, options) => {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await getAuthToken()}`,
'X-Trace-Id': crypto.randomUUID(),
...options?.headers,
},
body: JSON.stringify(body),
credentials: options?.credentials,
signal: options?.signal,
});
if (!response.ok) throw await response.json();
return response.json();
},
get: async (url, options) => {
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${await getAuthToken()}`,
...options?.headers,
},
credentials: options?.credentials,
signal: options?.signal,
});
if (!response.ok) throw await response.json();
return response.json();
},
},
});The SDK calls httpClient.post() / httpClient.get() for every request —
you control transport, headers, retries, and observability end-to-end.
Bring Your Own Logger
The SDK is silent by default — no console output of any level. Two ways to opt in:
Built-in console logger — set
debug: trueto enable verbose output (debug / info / warn / error all routed throughconsole.*, with phone numbers, JWTs, and session keys auto-redacted):const client = new PhoneAuthClient({ endpoints: { ... }, debug: true });Bring your own — forward SDK logs to your existing logging infrastructure (Sentry, Datadog, Pino, etc.) by providing a
loggerthat satisfies theLoggerinterface. Your logger receives every level the SDK emits; level filtering is your responsibility:const client = new PhoneAuthClient({ endpoints: { ... }, logger: { debug: (msg, data) => myLogger.debug('[SDK]', msg, data), info: (msg, data) => myLogger.info('[SDK]', msg, data), warn: (msg, data) => myLogger.warn('[SDK]', msg, data), error: (msg, data) => myLogger.error('[SDK]', msg, data), }, });
The SDK never logs caught errors before re-throwing — your try/catch
sees them once. Routine flow events (prepare success, phone retrieved,
desktop companion progress) are emitted at debug so they stay quiet
unless you explicitly want them. warn is reserved for genuinely
recoverable failures (modal failed to render, invocation report upload
failed).
Core Package
For advanced use cases, you can import just the types and validators without the full client:
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
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
type UseCase = 'GetPhoneNumber' | 'VerifyPhoneNumber';
// Or use the constant
USE_CASE.GET_PHONE_NUMBER // 'GetPhoneNumber'
USE_CASE.VERIFY_PHONE_NUMBER // 'VerifyPhoneNumber'Prepare Request
interface PrepareRequest {
use_case: UseCase;
phone_number?: string; // Required for VerifyPhoneNumber
parent_session_id?: string; // For cross-device flows
}Invoke Result
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:
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:
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:
// 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
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
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
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
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
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 | Eligible strategies | Requirements |
|---------|---------------------|--------------|
| Chrome Android 128+ | ts43 (when the carrier supports TS43) or link (mobile-web mode, when the carrier uses the App Clip / App Link flow) | Digital Credentials API |
| Safari iOS / Chrome iOS | link (mobile-web mode) | App Clip support on the device |
| Chrome / Edge / Firefox Desktop | desktop | Any modern version |
Strategy assignment is done by Glide's Magical Auth service in prepare (using the carrier + your user agent) — the same browser may receive different strategies for different carriers, and link runs in mobile-web mode on every supported mobile browser.
Development (contributors)
npm ci
npm test
npm run typecheck
npm run build # CJS, ESM, types, browser bundle
npm run clean
npm pack # for local install testingSupport
License
See LICENSE in this package.
