@hiyve/identity-client
Zero-dependency browser SDK for Hiyve Identity. Built on native fetch and localStorage.
Features
Authentication
- Register, login, logout -- email/password auth with automatic token storage
- Automatic token refresh -- refreshes JWT access tokens before they expire
- Persistent sessions -- tokens survive page reloads via
localStorage(or in-memory for SSR) - Password reset -- request reset emails and submit new passwords via token
- Email verification -- verify addresses and resend verification emails
MCP Tokens (Programmatic Access)
- Create tokens -- long-lived API tokens for CI/CD, integrations, and MCP server connections
- Permission scoping -- restrict tokens to specific capabilities (
mcp:data:read,mcp:search:execute, etc.) - IP restrictions -- limit token usage to specific CIDR ranges
- Lifecycle management -- list, validate, and revoke tokens
OAuth 2.1 (PKCE)
- Client management -- create, list, and delete OAuth clients with redirect URI validation
- Authorization Code flow -- full OAuth 2.1 with automatic PKCE S256 generation
- Token exchange -- exchange authorization codes for access/refresh token pairs
- Token refresh -- rotate OAuth refresh tokens
Two-Factor Authentication (Adaptive)
- Invisible device recognition -- known devices bypass 2FA automatically
- Email OTP verification -- 6-digit one-time codes for unknown devices
- Automatic device fingerprinting -- screen resolution, timezone, language, and platform signals
- TFA event system -- subscribe to
tfaRequiredevents for custom UI flows - Configurable per-org -- TFA and email verification can be independently enabled or disabled via organization settings (see Server Integration Guide)
Trusted Device Management
- List devices -- view all recognized devices for the authenticated user
- Revoke devices -- remove specific devices or revoke all at once
- Automatic registration -- devices are registered as trusted after successful OTP verification
User Profile
- Get profile -- fetch the full user profile (name, picture, phone, organization, metadata, roles)
- Update profile -- update display name, picture, phone, organization, and custom metadata
Developer Experience
- Zero runtime dependencies -- ships as ES module (~18 KB) and UMD (~9 KB)
- TypeScript definitions included
- Structured errors -- every failure is an
AuthErrorwith a typedcodeand optionaldetails - Event system -- subscribe to auth state changes, token refresh, and errors
- Framework-agnostic -- works with React, Vue, vanilla JS, or any browser environment
Table of Contents
- Installation
- Quick Start
- Configuration
- Authentication Flows
- Password Reset
- Email Verification
- MCP Tokens
- OAuth 2.1 (PKCE)
- Two-Factor Authentication
- Trusted Device Management
- Auth State and Events
- Token Management
- Error Handling
- Advanced Usage
- API Reference
- Security & Compliance
- Browser Support
- License
Installation
npm install @hiyve/identity-client
UMD (script tag)
<script src="path/to/hiyve-identity-client.umd.cjs"></script>
<script>
const auth = new HiyveIdentityClient.HiyveAuth({
apiKey: 'pk_live_your_key',
});
</script>
Quick Start
import { HiyveAuth } from '@hiyve/identity-client';
const auth = new HiyveAuth({
apiKey: 'pk_live_your_api_key',
});
// Register a new user
await auth.register({
email: 'user@example.com',
password: 'securePassword123',
name: 'Jane Doe',
});
// Log in (may trigger TFA on unknown devices)
const result = await auth.login({
email: 'user@example.com',
password: 'securePassword123',
});
if (result.tfaRequired) {
// Unknown device -- OTP code sent to user's email
console.log('Check your email for a verification code');
// After user enters the code:
const { user } = await auth.verifyTfa({ code: '123456' });
console.log('Logged in as:', user.email);
} else {
console.log('Logged in as:', result.user.email);
}
console.log('Authenticated:', auth.isAuthenticated()); // true
// Listen for auth state changes
const unsubscribe = auth.onAuthStateChange(({ authenticated, user }) => {
console.log('Auth state changed:', authenticated, user);
});
// Log out
await auth.logout();
// Clean up when done (e.g. component unmount)
unsubscribe();
auth.destroy();
Configuration
Pass a config object to the HiyveAuth constructor:
const auth = new HiyveAuth({
// Required
apiKey: 'pk_live_your_api_key', // Your public API key
// Optional (defaults shown)
environment: 'production', // 'production' or 'development'
basePath: '/identity/auth', // API route prefix
tokenStorage: 'localStorage', // 'localStorage' or 'memory'
autoRefresh: true, // Auto-refresh tokens before expiry
refreshBuffer: 300, // Seconds before expiry to trigger refresh
timeout: 30000, // Request timeout in milliseconds
});
| Option | Type | Default | Description |
|---|---|---|---|
apiKey |
string |
required* | Public API key (pk_live_* or pk_test_*) |
baseUrl |
string |
— | Full base URL for proxy mode (e.g. '/api/hiyve/identity'). When set, apiKey is not required |
environment |
string |
'production' |
'production' or 'development' |
basePath |
string |
'/identity/auth' |
API route prefix (ignored when baseUrl is set) |
tokenStorage |
string |
'localStorage' |
'localStorage' for persistent sessions, 'memory' for ephemeral. Production recommendation: use 'memory' to protect refresh tokens from XSS — see Security & Compliance |
autoRefresh |
boolean |
true |
Automatically refresh tokens before they expire |
refreshBuffer |
number |
300 |
How many seconds before expiry to trigger auto-refresh |
timeout |
number |
30000 |
HTTP request timeout in milliseconds |
*apiKey is required unless baseUrl is provided for proxy mode.
Authentication Flows
Register
Create a new user account. Depending on the organization's emailVerificationRequired setting, the server may send a verification email or mark the user as verified immediately.
try {
const { user } = await auth.register({
email: 'user@example.com',
password: 'securePassword123',
name: 'Jane Doe', // optional
metadata: { role: 'viewer' }, // optional -- custom data
});
console.log('Registered:', user.id);
if (user.emailVerified) {
// Email verification not required -- user can log in immediately
console.log('Ready to log in');
} else {
// Email verification required -- tell user to check their inbox
console.log('Check your email for a verification link');
}
} catch (err) {
if (err.code === 'USER_ALREADY_EXISTS') {
console.log('Email already registered');
}
}
Note: Registration does not automatically log the user in. Call auth.login() after registration.
Login
Authenticate with email and password. On success, tokens are stored automatically and auto-refresh is started.
try {
const { user, accessToken, refreshToken, expiresIn } = await auth.login({
email: 'user@example.com',
password: 'securePassword123',
});
console.log('Welcome,', user.name);
console.log('Token expires in', expiresIn, 'seconds');
} catch (err) {
if (err.code === 'INVALID_CREDENTIALS') {
console.log('Wrong email or password');
} else if (err.code === 'EMAIL_NOT_VERIFIED') {
console.log('Please verify your email first');
}
}
Login with TFA
When a user logs in from an unrecognized device, the server returns a TFA challenge instead of tokens. The SDK stores the challenge internally and emits a tfaRequired event.
const result = await auth.login({
email: 'user@example.com',
password: 'securePassword123',
});
if (result.tfaRequired) {
// The response includes:
// - challengeToken: stored internally by the SDK
// - methods: ['email_otp'] -- available verification methods
// - expiresIn: seconds until the challenge expires (default: 600)
// - user: { id, email, name } -- the user being authenticated
console.log('TFA required, methods:', result.methods);
console.log('Code expires in', result.expiresIn, 'seconds');
// Prompt the user for the OTP code from their email, then:
const { user, accessToken, deviceId } = await auth.verifyTfa({ code: '123456' });
console.log('Logged in as:', user.email);
// The device is now trusted -- future logins from this device skip TFA
} else {
// Known device -- direct login (no TFA needed)
const { user, accessToken } = result;
console.log('Logged in as:', user.email);
}
Subsequent logins from the same device (within 90 days) will bypass TFA automatically.
Logout
Invalidates the session server-side and clears all local tokens. The server call is best-effort -- local state is always cleared even if the network request fails.
await auth.logout();
console.log(auth.isAuthenticated()); // false
Get Current User
Fetch the authenticated user's profile from the server.
const { user } = await auth.getUser();
console.log(user.email, user.name);
This requires a valid access token. If the token is expired and auto-refresh is enabled, the SDK will attempt to refresh it before the request.
User Profile
Fetch or update the authenticated user's full profile, which includes additional fields beyond what getUser() returns (picture, phone, organization, metadata, roles, etc.).
Get Profile
const { profile } = await auth.getProfile();
console.log(profile.name, profile.email);
console.log(profile.organization, profile.phone);
console.log(profile.metadata); // custom key-value data
Update Profile
const { profile } = await auth.updateProfile({
name: 'Jane Smith',
phone: '+1234567890',
organization: 'Acme Corp',
metadata: { department: 'Engineering', theme: 'dark' },
});
console.log('Updated:', profile.name);
Updatable fields: name, picture, phone, organization, and metadata. Roles cannot be changed via this method.
Refresh Tokens
Manually refresh access and refresh tokens. This is handled automatically when autoRefresh is enabled, but you can call it explicitly if needed.
const { accessToken, refreshToken, expiresIn } = await auth.refreshTokens();
If the refresh token is invalid or expired, this throws an AuthError with code TOKEN_REFRESH_FAILED and clears the local auth state.
Password Reset
Request a Reset Email
await auth.requestPasswordReset({ email: 'user@example.com' });
// Server sends an email with a reset link containing a token
Reset Password with Token
Extract the token from the reset link (typically a URL query parameter) and submit the new password:
await auth.resetPassword({
token: 'reset_token_from_email_link',
password: 'newSecurePassword456',
});
Email Verification
Verify Email
Extract the token from the verification link and submit it:
await auth.verifyEmail({ token: 'verification_token_from_email' });
Resend Verification Email
await auth.resendVerification({ email: 'user@example.com' });
MCP Tokens
MCP tokens are long-lived API tokens for programmatic access (CI/CD pipelines, integrations, MCP server connections). All MCP token methods require the user to be logged in.
Create a Token
const { rawToken, token } = await auth.createMCPToken({
name: 'CI/CD Pipeline',
permissions: ['mcp:data:read', 'mcp:search:execute'],
expiresIn: '90d',
ipRestriction: ['203.0.113.0/24'],
});
// Store rawToken securely -- it cannot be retrieved again
console.log('Token:', rawToken);
console.log('Token ID:', token.id);
List Tokens
const { tokens } = await auth.listMCPTokens();
tokens.forEach((t) => {
console.log(t.id, t.metadata.name, t.expiresAt);
});
Validate a Token
const { valid, token } = await auth.validateMCPToken(rawToken);
if (valid) {
console.log('Token is valid, permissions:', token.permissions);
}
Revoke a Token
await auth.revokeMCPToken(tokenId);
OAuth 2.1 (PKCE)
The SDK supports the full OAuth 2.1 Authorization Code flow with PKCE (S256). This enables secure third-party integrations.
Client Management
Create and manage OAuth clients. All client management methods require the user to be logged in.
// Create a client
const { clientId, clientSecret, client } = await auth.createOAuthClient({
clientName: 'My Integration',
redirectUris: ['https://myapp.com/callback'],
scopes: ['mcp:data:read'],
});
// List clients
const { clients } = await auth.listOAuthClients();
// Get a specific client
const { client: details } = await auth.getOAuthClient(clientId);
// Delete a client
await auth.deleteOAuthClient(clientId);
Authorization Flow
The full OAuth 2.1 flow with PKCE:
// Step 1: Create an authorization code (user must be logged in)
// PKCE verifier/challenge are generated automatically
const { code, codeVerifier, state } = await auth.createAuthorizationCode({
clientId: 'your-client-id',
redirectUri: 'https://myapp.com/callback',
scope: 'mcp:data:read',
state: 'random-state-value',
});
// Step 2: Exchange the code for tokens (no login required)
const tokens = await auth.exchangeAuthorizationCode({
code,
redirectUri: 'https://myapp.com/callback',
codeVerifier,
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
});
console.log(tokens.access_token);
console.log(tokens.refresh_token);
console.log(tokens.expires_in);
// Step 3: Refresh OAuth tokens when they expire
const newTokens = await auth.refreshOAuthToken({
refreshToken: tokens.refresh_token,
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
});
Two-Factor Authentication
The SDK supports adaptive two-factor authentication. When TFA is enabled on the organization (settings.tfa.enabled), unknown devices trigger an email OTP challenge. Known (trusted) devices bypass TFA automatically. When TFA is disabled, login() always returns tokens directly.
TFA and email verification are independent settings -- you can use either, both, or neither. See the Server Integration Guide for configuration details.
Handling TFA Challenge
Use the onTfaRequired event to react when login triggers a TFA challenge:
// Listen for TFA events before calling login
const unsubscribe = auth.onTfaRequired(({ methods, expiresIn, user }) => {
console.log('TFA required for', user.email);
console.log('Available methods:', methods); // ['email_otp']
console.log('Code expires in', expiresIn, 'seconds');
// Show your OTP input UI here
});
const result = await auth.login({ email, password });
if (result.tfaRequired) {
// The onTfaRequired callback has already fired
// Wait for user to enter their OTP code...
}
Verifying OTP
After the user receives and enters the 6-digit code from their email:
try {
const { user, accessToken, deviceId } = await auth.verifyTfa({
code: '123456',
type: 'email_otp', // optional, defaults to 'email_otp'
});
console.log('Authenticated:', user.email);
console.log('Device trusted:', deviceId);
// This device is now registered as trusted for future logins
} catch (err) {
switch (err.code) {
case 'TFA_INVALID_CODE':
console.log('Wrong code, try again');
break;
case 'TFA_CHALLENGE_EXPIRED':
console.log('Code expired, please log in again');
break;
case 'TFA_CHALLENGE_LOCKED':
console.log('Too many attempts, please log in again');
break;
case 'INVALID_STATE':
console.log('No pending TFA challenge -- call login() first');
break;
}
}
Resending OTP
If the user didn't receive the code or it expired:
const { message, expiresIn } = await auth.resendOtp();
console.log(message); // 'New code sent'
console.log('New code expires in', expiresIn, 'seconds');
Trusted Device Management
After successful TFA verification, the device is automatically registered as trusted. Users can view and manage their trusted devices.
List Trusted Devices
const { devices } = await auth.listTrustedDevices();
devices.forEach((device) => {
console.log(device.deviceId, device.deviceName, device.lastUsedAt);
});
Revoke a Specific Device
await auth.revokeTrustedDevice('device-id-to-revoke');
// The device will require TFA on next login
Revoke All Devices
const { count } = await auth.revokeAllTrustedDevices();
console.log(`Revoked ${count} devices`);
// All devices (including current) will require TFA on next login
Auth State and Events
Check Authentication Status
isAuthenticated() returns true if there is a non-expired access token stored locally. It does not make a network request.
if (auth.isAuthenticated()) {
// User has a valid token
}
Get Access Token
Retrieve the raw access token string for use in custom API calls:
const token = auth.getAccessToken();
if (token) {
fetch('https://your-api-host.com/api/data', {
headers: { Authorization: `Bearer ${token}` },
});
}
Listen for Auth State Changes
The authStateChange event fires on login, logout, and when a token refresh fails (which clears auth state):
const unsubscribe = auth.onAuthStateChange(({ authenticated, user }) => {
if (authenticated) {
console.log('Signed in as', user.email);
} else {
console.log('Signed out');
}
});
// Later: stop listening
unsubscribe();
Events Reference
| Event | Payload | When |
|---|---|---|
authStateChange |
{ authenticated: boolean, user: object | null } |
Login, logout, or refresh failure |
tokenRefreshed |
{ accessToken: string, expiresIn: number } |
Successful token refresh |
error |
AuthError |
Token refresh failure |
tfaRequired |
{ challengeToken: string, methods: string[], expiresIn: number, user: object } |
Login requires TFA verification |
Token Management
The SDK handles tokens automatically, but here is a summary of the behavior:
- Persistence: Sessions survive page reloads and new tabs when using
localStoragestorage (the default). - Auto-refresh: When enabled, tokens are refreshed
refreshBufferseconds before expiry. No action is needed from your code. - Expiry check:
isAuthenticated()returns whether the access token is still valid, without making a network request. - Failure handling: If auto-refresh fails, auth state is cleared and an
authStateChangeevent fires with{ authenticated: false }. - Memory storage: Use
tokenStorage: 'memory'for environments wherelocalStorageis unavailable (SSR, tests, private browsing fallback). Tokens are lost on page reload. For production apps,'memory'is recommended — it keeps refresh tokens out oflocalStorage, where they could be exfiltrated by XSS. The trade-off is that users must re-authenticate after a page reload.
Error Handling
All SDK methods throw AuthError instances on failure. Each error has a code property from the AUTH_ERRORS constant, a human-readable message, and optionally a statusCode and details object.
import { HiyveAuth, AuthError, AUTH_ERRORS } from '@hiyve/identity-client';
try {
await auth.login({ email: 'user@example.com', password: 'wrong' });
} catch (err) {
if (err instanceof AuthError) {
switch (err.code) {
case AUTH_ERRORS.INVALID_CREDENTIALS:
showError('Invalid email or password');
break;
case AUTH_ERRORS.EMAIL_NOT_VERIFIED:
showError('Please verify your email');
break;
case AUTH_ERRORS.RATE_LIMITED:
showError('Too many attempts. Please wait.');
break;
case AUTH_ERRORS.NETWORK_ERROR:
showError('Network error. Check your connection.');
break;
case AUTH_ERRORS.TIMEOUT:
showError('Request timed out. Try again.');
break;
default:
showError(err.message);
}
}
}
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
NETWORK_ERROR |
-- | Network failure (no response received) |
TIMEOUT |
-- | Request exceeded the timeout |
INVALID_CREDENTIALS |
401 | Wrong email or password |
TOKEN_EXPIRED |
401 | Access token has expired |
TOKEN_REFRESH_FAILED |
varies | Refresh token is invalid or expired |
UNAUTHORIZED |
401 | Generic authentication failure |
FORBIDDEN |
403 | Insufficient permissions |
NOT_FOUND |
404 | Resource not found |
RATE_LIMITED |
429 | Too many requests |
VALIDATION_ERROR |
400 | Invalid input (check err.details) |
EMAIL_NOT_VERIFIED |
401 | Email address not yet verified |
USER_ALREADY_EXISTS |
409 | Email already registered |
SERVER_ERROR |
5xx | Unexpected server error |
INVALID_CONFIG |
-- | Bad SDK configuration or destroyed instance |
INVALID_TOKEN |
-- | Token is malformed or unreadable |
INVALID_GRANT |
400 | Invalid authorization code or grant type |
INVALID_SCOPE |
400 | Requested scope is not allowed |
TFA_REQUIRED |
200 | Login requires two-factor authentication |
TFA_INVALID_CODE |
401 | OTP code is incorrect |
TFA_CHALLENGE_EXPIRED |
401 | TFA challenge has expired |
TFA_CHALLENGE_LOCKED |
429 | Too many failed OTP attempts |
INVALID_STATE |
-- | No pending TFA challenge (call login() first) |
Validation Error Details
When code is VALIDATION_ERROR, the details property contains field-level errors from the server:
try {
await auth.register({ email: 'bad', password: '123' });
} catch (err) {
if (err.code === AUTH_ERRORS.VALIDATION_ERROR) {
console.log(err.details);
// e.g. { email: '"email" must be a valid email', password: 'minimum 8 characters' }
}
}
Serialization
AuthError instances have a toJSON() method for logging:
catch (err) {
console.log(JSON.stringify(err));
// { "name": "AuthError", "code": "INVALID_CREDENTIALS", "message": "...", "statusCode": 401, "details": null }
}
Advanced Usage
Using HttpClient Directly
If you need to make custom requests to additional API endpoints that share the same API key and auth headers:
import { HttpClient } from '@hiyve/identity-client';
const client = new HttpClient({
baseUrl: 'https://your-api-host.com/api',
apiKey: 'pk_live_your_api_key',
timeout: 15000,
getAccessToken: () => auth.getAccessToken(),
});
// Unauthenticated request
const data = await client.get('/public/info');
// Authenticated request
const profile = await client.get('/profile', { auth: true });
// POST with body
const result = await client.post('/items', { name: 'New Item' }, { auth: true });
// PUT with body
await client.put('/items/123', { name: 'Updated Item' }, { auth: true });
// DELETE request
await client.delete('/items/123', { auth: true });
Every request automatically includes the X-Hiyve-Api-Key header. When { auth: true } is passed, the Authorization: Bearer <token> header is included. HTTP errors are mapped to AuthError instances.
Using TokenManager Directly
For advanced use cases (e.g., building a custom auth flow or integrating with a state management library):
import { TokenManager } from '@hiyve/identity-client';
const tokens = new TokenManager({
storage: 'localStorage', // or 'memory'
autoRefresh: true,
refreshBuffer: 120, // refresh 2 minutes before expiry
});
// Store tokens from your own login endpoint
tokens.setTokens({ accessToken: 'eyJ...', refreshToken: 'eyJ...' });
// Check state
tokens.isAuthenticated(); // true if access token is not expired
tokens.getAccessToken(); // raw JWT string or null
tokens.getRefreshToken(); // raw refresh token string or null
// Decode JWT payload (no signature verification)
const payload = tokens.decodeToken(tokens.getAccessToken());
console.log(payload.sub, payload.email, payload.exp);
// Get expiry as Unix timestamp (seconds)
const exp = tokens.getTokenExpiry(tokens.getAccessToken());
console.log('Expires at:', new Date(exp * 1000));
// Clear everything
tokens.clear();
Using PKCEHelper Directly
If you need to generate PKCE pairs for custom OAuth flows:
import { PKCEHelper } from '@hiyve/identity-client';
// Generate a PKCE pair
const { codeVerifier, codeChallenge } = await PKCEHelper.generatePKCEPair();
// Or generate components individually
const verifier = PKCEHelper.generateCodeVerifier();
const challenge = await PKCEHelper.generateCodeChallenge(verifier);
Generates cryptographically secure PKCE pairs for OAuth 2.1 flows.
Using DeviceFingerprint Directly
If you need to access device fingerprinting independently:
import { DeviceFingerprint } from '@hiyve/identity-client';
// Collect browser signals
const fingerprint = DeviceFingerprint.collect();
console.log(fingerprint);
// { screenResolution: '1920x1080', timezone: 'America/New_York', language: 'en-US', platform: 'MacIntel' }
// Get or create a persistent device ID
const deviceId = DeviceFingerprint.getDeviceId('localStorage', 'myapp_');
// Encode fingerprint for transport
const encoded = DeviceFingerprint.encode(fingerprint);
// Base64-encoded JSON string
Login requests automatically include device-recognition headers so the server can identify trusted devices.
Memory Storage
Use in-memory storage for server-side rendering, testing, or environments without localStorage:
const auth = new HiyveAuth({
apiKey: 'pk_test_your_api_key',
tokenStorage: 'memory',
});
If localStorage is configured but unavailable at runtime (e.g., private browsing restrictions), the SDK automatically falls back to memory storage.
Custom Base Path
If your backend mounts the auth routes at a different path:
const auth = new HiyveAuth({
apiKey: 'pk_live_your_api_key',
basePath: '/v2/auth', // overrides the default /identity/auth path
});
Framework Integration (React)
A typical pattern for React applications:
import { useEffect, useState, createContext, useContext } from 'react';
import { HiyveAuth } from '@hiyve/identity-client';
// Create a singleton instance
const auth = new HiyveAuth({
apiKey: 'pk_live_your_api_key',
});
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [state, setState] = useState({
authenticated: auth.isAuthenticated(),
user: null,
loading: true,
});
useEffect(() => {
// Listen for auth changes
const unsubscribe = auth.onAuthStateChange(({ authenticated, user }) => {
setState({ authenticated, user, loading: false });
});
// Load user if we have a token
if (auth.isAuthenticated()) {
auth.getUser()
.then(({ user }) => setState({ authenticated: true, user, loading: false }))
.catch(() => setState({ authenticated: false, user: null, loading: false }));
} else {
setState((s) => ({ ...s, loading: false }));
}
return () => {
unsubscribe();
auth.destroy();
};
}, []);
return (
<AuthContext.Provider value={{ ...state, auth }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
Usage in a component:
function LoginPage() {
const { auth } = useAuth();
const [tfaPending, setTfaPending] = useState(false);
useEffect(() => {
return auth.onTfaRequired(() => setTfaPending(true));
}, []);
async function handleLogin(email, password) {
try {
const result = await auth.login({ email, password });
if (result.tfaRequired) {
// TFA UI will show via onTfaRequired callback
}
// If no TFA, AuthProvider updates state via onAuthStateChange
} catch (err) {
// Handle error
}
}
async function handleVerifyOtp(code) {
try {
await auth.verifyTfa({ code });
setTfaPending(false);
// AuthProvider updates state via onAuthStateChange
} catch (err) {
// Handle TFA_INVALID_CODE, TFA_CHALLENGE_EXPIRED, etc.
}
}
if (tfaPending) {
return <OtpForm onSubmit={handleVerifyOtp} />;
}
// ... render login form
}
function Dashboard() {
const { authenticated, user, loading } = useAuth();
if (loading) return <div>Loading...</div>;
if (!authenticated) return <Navigate to="/login" />;
return <div>Welcome, {user.name}</div>;
}
API Reference
HiyveAuth
The main SDK class. Coordinates authentication flows, token storage, and event emission.
Constructor
new HiyveAuth(config: HiyveAuthConfig)
Throws AuthError with code INVALID_CONFIG if neither apiKey nor baseUrl is provided, or if environment is invalid.
Methods
| Method | Returns | Description |
|---|---|---|
register({ email, password, name?, metadata? }) |
Promise<{ user }> |
Register a new user |
login({ email, password }) |
Promise<LoginResult> |
Log in (may return tokens or TFA challenge) |
logout() |
Promise<void> |
Log out and clear tokens |
refreshTokens() |
Promise<{ accessToken, refreshToken, expiresIn }> |
Manually refresh tokens |
getUser() |
Promise<{ user }> |
Fetch authenticated user's basic identity |
getProfile() |
Promise<{ profile }> |
Fetch the full user profile (name, picture, phone, organization, metadata, roles) |
updateProfile({ name?, picture?, phone?, organization?, metadata? }) |
Promise<{ profile }> |
Update the user's own profile |
requestPasswordReset({ email }) |
Promise<{ message }> |
Send password reset email |
resetPassword({ token, password }) |
Promise<{ message }> |
Reset password with token |
verifyEmail({ token }) |
Promise<{ message }> |
Verify email address |
resendVerification({ email }) |
Promise<{ message }> |
Resend verification email |
createMCPToken({ name?, permissions?, expiresIn?, ipRestriction? }) |
Promise<{ rawToken, token }> |
Create an MCP token |
listMCPTokens() |
Promise<{ tokens }> |
List MCP tokens |
revokeMCPToken(tokenId) |
Promise<{ message }> |
Revoke an MCP token |
validateMCPToken(token) |
Promise<{ valid, token? }> |
Validate a raw MCP token |
createOAuthClient({ clientName, redirectUris, scopes? }) |
Promise<{ clientId, clientSecret, client }> |
Create an OAuth client |
listOAuthClients() |
Promise<{ clients }> |
List OAuth clients |
getOAuthClient(clientId) |
Promise<{ client }> |
Get an OAuth client |
deleteOAuthClient(clientId) |
Promise<{ message }> |
Delete an OAuth client |
createAuthorizationCode({ clientId, redirectUri, scope?, state? }) |
Promise<{ code, codeVerifier, codeChallenge, state }> |
Create authorization code with auto-PKCE |
exchangeAuthorizationCode({ code, redirectUri, codeVerifier, clientId, clientSecret }) |
Promise<OAuthTokenResponse> |
Exchange code for tokens |
refreshOAuthToken({ refreshToken, clientId, clientSecret }) |
Promise<OAuthTokenResponse> |
Refresh OAuth tokens |
isAuthenticated() |
boolean |
Check if access token exists and is not expired |
getAccessToken() |
string | null |
Get the raw access token |
onAuthStateChange(callback) |
() => void |
Subscribe to auth state changes; returns unsubscribe function |
verifyTfa({ code, type? }) |
Promise<{ user, accessToken, refreshToken, expiresIn, deviceId }> |
Verify OTP code for pending TFA challenge |
resendOtp() |
Promise<{ message, expiresIn }> |
Resend OTP email for pending challenge |
onTfaRequired(callback) |
() => void |
Subscribe to TFA challenge events; returns unsubscribe function |
listTrustedDevices() |
Promise<{ devices }> |
List trusted devices for authenticated user |
revokeTrustedDevice(deviceId) |
Promise<{ message }> |
Revoke a specific trusted device |
revokeAllTrustedDevices() |
Promise<{ message, count }> |
Revoke all trusted devices |
destroy() |
void |
Stop auto-refresh, remove all listeners. Instance is unusable after this. |
TokenManager
Manages token storage, JWT payload decoding, and auto-refresh scheduling.
Constructor
new TokenManager(options?: TokenManagerOptions)
| Option | Type | Default | Description |
|---|---|---|---|
storage |
string |
'localStorage' |
'localStorage' or 'memory' |
storagePrefix |
string |
-- | Optional key prefix for localStorage |
autoRefresh |
boolean |
true |
Enable auto-refresh scheduling |
refreshBuffer |
number |
300 |
Seconds before expiry to trigger refresh |
Methods
| Method | Returns | Description |
|---|---|---|
setTokens({ accessToken, refreshToken }) |
void |
Store both tokens |
getAccessToken() |
string | null |
Retrieve the access token |
getRefreshToken() |
string | null |
Retrieve the refresh token |
clear() |
void |
Remove all tokens and stop auto-refresh |
isAuthenticated() |
boolean |
Check if access token exists and is not expired |
getTokenExpiry(token) |
number | null |
Decode exp from JWT (seconds since epoch) |
decodeToken(token) |
object | null |
Decode full JWT payload (no signature verification) |
startAutoRefresh(refreshFn) |
void |
Start auto-refresh with the given async function |
stopAutoRefresh() |
void |
Cancel the auto-refresh timer |
HttpClient
Fetch wrapper that adds API key headers, authorization, timeouts, and error mapping.
Constructor
new HttpClient(options: HttpClientOptions)
| Option | Type | Default | Description |
|---|---|---|---|
baseUrl |
string |
required | Full base URL for requests |
apiKey |
string |
required | API key sent as X-Hiyve-Api-Key header |
timeout |
number |
30000 |
Request timeout in milliseconds |
getAccessToken |
() => string | null |
() => null |
Function returning current access token |
Methods
| Method | Returns | Description |
|---|---|---|
request(method, path, body?, options?) |
Promise<object> |
Make an HTTP request |
get(path, options?) |
Promise<object> |
GET request |
post(path, body?, options?) |
Promise<object> |
POST request |
put(path, body?, options?) |
Promise<object> |
PUT request |
delete(path, options?) |
Promise<object> |
DELETE request |
Request options:
| Option | Type | Default | Description |
|---|---|---|---|
auth |
boolean |
false |
Include Authorization: Bearer <token> header |
timeout |
number |
-- | Override the default timeout for this request |
headers |
Record<string, string> |
-- | Additional headers to merge into the request |
All requests include credentials: 'include' for cookie-based flows and Content-Type: application/json.
PKCEHelper
Static utility class for generating PKCE (Proof Key for Code Exchange) parameters for OAuth 2.1 authorization flows. Uses the Web Crypto API.
| Method | Returns | Description |
|---|---|---|
PKCEHelper.generateCodeVerifier() |
string |
Generate a cryptographically random code verifier (43+ chars, base64url) |
PKCEHelper.generateCodeChallenge(verifier) |
Promise<string> |
Compute S256 code challenge from a verifier (base64url-encoded SHA-256) |
PKCEHelper.generatePKCEPair() |
Promise<{ codeVerifier, codeChallenge }> |
Generate both verifier and challenge |
DeviceFingerprint
Static utility class for browser device fingerprinting. Used automatically by HiyveAuth.login() to send device recognition headers.
| Method | Returns | Description |
|---|---|---|
DeviceFingerprint.collect() |
DeviceFingerprintData |
Collect browser signals (screen, timezone, language, platform) |
DeviceFingerprint.getDeviceId(storageType?, prefix?) |
string | null |
Get stored device ID from localStorage or memory |
DeviceFingerprint.setDeviceId(storageType?, prefix?, id?) |
void |
Store a device ID |
DeviceFingerprint.encode(fingerprint) |
string |
Base64-encode fingerprint data for HTTP headers |
EventEmitter
Minimal pub/sub event system used internally by HiyveAuth. Also exported for custom use.
| Method | Returns | Description |
|---|---|---|
on(event, callback) |
() => void |
Subscribe; returns unsubscribe function |
emit(event, data?) |
void |
Emit an event to all subscribers |
removeAllListeners(event?) |
void |
Remove listeners for one event, or all events |
listenerCount(event) |
number |
Count of listeners for an event |
AuthError
Custom error class extending Error. All SDK errors are AuthError instances.
| Property | Type | Description |
|---|---|---|
name |
string |
Always 'AuthError' |
code |
AuthErrorCode |
Error code from AUTH_ERRORS |
message |
string |
Human-readable message |
statusCode |
number | null |
HTTP status code (if applicable) |
details |
object | null |
Additional details (e.g., validation errors) |
Methods: toJSON() returns a plain object with all properties.
The AuthErrorCode type is exported as (typeof AUTH_ERRORS)[keyof typeof AUTH_ERRORS] — useful for typed error-code parameters.
AUTH_ERRORS
Constant object mapping error names to string codes. Use for switch/if comparisons:
import { AUTH_ERRORS } from '@hiyve/identity-client';
AUTH_ERRORS.NETWORK_ERROR // 'NETWORK_ERROR'
AUTH_ERRORS.TIMEOUT // 'TIMEOUT'
AUTH_ERRORS.INVALID_CREDENTIALS // 'INVALID_CREDENTIALS'
AUTH_ERRORS.TOKEN_EXPIRED // 'TOKEN_EXPIRED'
AUTH_ERRORS.TOKEN_REFRESH_FAILED // 'TOKEN_REFRESH_FAILED'
AUTH_ERRORS.UNAUTHORIZED // 'UNAUTHORIZED'
AUTH_ERRORS.FORBIDDEN // 'FORBIDDEN'
AUTH_ERRORS.NOT_FOUND // 'NOT_FOUND'
AUTH_ERRORS.RATE_LIMITED // 'RATE_LIMITED'
AUTH_ERRORS.VALIDATION_ERROR // 'VALIDATION_ERROR'
AUTH_ERRORS.EMAIL_NOT_VERIFIED // 'EMAIL_NOT_VERIFIED'
AUTH_ERRORS.USER_ALREADY_EXISTS // 'USER_ALREADY_EXISTS'
AUTH_ERRORS.SERVER_ERROR // 'SERVER_ERROR'
AUTH_ERRORS.INVALID_CONFIG // 'INVALID_CONFIG'
AUTH_ERRORS.INVALID_TOKEN // 'INVALID_TOKEN'
AUTH_ERRORS.INVALID_GRANT // 'INVALID_GRANT'
AUTH_ERRORS.INVALID_SCOPE // 'INVALID_SCOPE'
AUTH_ERRORS.TFA_REQUIRED // 'TFA_REQUIRED'
AUTH_ERRORS.TFA_INVALID_CODE // 'TFA_INVALID_CODE'
AUTH_ERRORS.TFA_CHALLENGE_EXPIRED // 'TFA_CHALLENGE_EXPIRED'
AUTH_ERRORS.TFA_CHALLENGE_LOCKED // 'TFA_CHALLENGE_LOCKED'
AUTH_ERRORS.INVALID_STATE // 'INVALID_STATE'
Security & Compliance
The Hiyve Identity system passes 9 of 10 OWASP Top 10 (2021) categories with full compliance, covering broken access control, cryptographic controls, injection prevention, adaptive TFA, multi-tenant isolation, and layered rate limiting. One category (Security Logging & Monitoring) has partial compliance with structured audit logging on the roadmap.
Key security highlights:
- Layered rate limiting — Redis-backed, per-tenant, enforced across clustered deployments
- Adaptive TFA — device fingerprinting with automatic OTP challenge for unknown devices
- CSRF protection — HMAC-SHA256 double-submit cookies with timing-safe validation
- Zero client dependencies — no supply-chain risk from transitive packages
- Multi-tenant isolation — organization scoping on all queries, tokens, and rate-limit keys
Token storage advisory: The default
tokenStorage: 'localStorage'persists refresh tokens inlocalStorage, making them accessible to any JavaScript on the same origin. If your site includes third-party scripts (analytics, ads, support widgets), consider usingtokenStorage: 'memory'to keep refresh tokens out of reach of XSS. The trade-off is that sessions will not survive page reloads. When using server-side proxy mode (baseUrl), you can implementhttpOnlycookie-based refresh on your server for the strongest protection.
See COMPLIANCE_REPORT.md for the full compliance report including OWASP mapping, cryptographic controls, architecture diagrams, open items, and a consumer security checklist.
Requirements
Browser Support
The SDK uses standard web APIs available in all modern browsers:
fetchandResponseAbortControlleratoblocalStorage(with automatic fallback to memory)setTimeout/clearTimeoutcrypto.subtleandcrypto.getRandomValues(for PKCE generation)
Minimum browser versions: Chrome 66+, Firefox 57+, Safari 12+, Edge 79+
No polyfills are required for modern browsers. For legacy environments, you may need polyfills for fetch and AbortController.
License
Commercial - IWantToPractice, LLC