Home

@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 tfaRequired events 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 AuthError with a typed code and optional details
  • 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

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 localStorage storage (the default).
  • Auto-refresh: When enabled, tokens are refreshed refreshBuffer seconds 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 authStateChange event fires with { authenticated: false }.
  • Memory storage: Use tokenStorage: 'memory' for environments where localStorage is unavailable (SSR, tests, private browsing fallback). Tokens are lost on page reload. For production apps, 'memory' is recommended — it keeps refresh tokens out of localStorage, 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 in localStorage, making them accessible to any JavaScript on the same origin. If your site includes third-party scripts (analytics, ads, support widgets), consider using tokenStorage: '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 implement httpOnly cookie-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:

  • fetch and Response
  • AbortController
  • atob
  • localStorage (with automatic fallback to memory)
  • setTimeout / clearTimeout
  • crypto.subtle and crypto.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