@hawcx/core
v2.4.0
Published
Headless Hawcx client that talks to the authentication API and exposes a typed flow state machine.
Keywords
Readme
@hawcx/core
Headless TypeScript client for Hawcx Auth. Manages the auth state machine, device trust crypto, and server communication. Platform-agnostic with no UI dependencies.
Installation
npm install @hawcx/coreQuick Start
import { createHawcxClient } from '@hawcx/core';
const client = createHawcxClient({
configId: 'your-config-id',
});
// Start signin flow
const state = await client.start('signin', '[email protected]');
// Check what the server wants
if (state.status === 'step') {
console.log('Render UI for:', state.step.type);
}How It Works
The SDK is a state machine. You send actions, the server responds with steps that tell you what UI to render.
idle → loading → step → loading → step → ... → completed
↓
errorEvery method returns AuthState:
type AuthState =
| { status: 'idle' }
| { status: 'loading'; session?: string }
| { status: 'step'; session: string; step: AuthStep }
| { status: 'completed'; session: string; authCode: string; expiresAt: string; codeVerifier?: string }
| { status: 'error'; session?: string; error: AuthError }Configuration
const client = createHawcxClient({
// Required - your Hawcx configuration ID
configId: 'your-config-id',
// Optional - API base URL (default: https://api.hawcx.com/v1)
apiBase: 'http://localhost:7998/v1',
// Optional - logger for debugging
logger: console,
// Optional - request timeout in ms (default: 10000)
timeout: 15000,
});Client Methods
start(flowType, identifier, device?)
Begin an authentication flow.
// Signin
await client.start('signin', '[email protected]');
// Signup
await client.start('signup', '[email protected]');
// Account management (step-up auth for settings changes)
await client.start('account_manage', '[email protected]');selectMethod(methodId)
Choose an auth method when step.type === 'select_method'.
await client.selectMethod('email_otp');submitCode(code)
Submit OTP code for enter_code steps.
await client.submitCode('123456');submitTotp(code)
Submit TOTP code for enter_totp or setup_totp steps.
await client.submitTotp('123456');submitPhone(phone)
Submit phone number for setup_sms steps (SMS enrollment).
await client.submitPhone('+14155551234');resend()
Request a new OTP code.
await client.resend();cancel()
Cancel the current flow.
await client.cancel();reset()
Reset to idle state and clear session data.
client.reset();signOut()
Sign out and clear all session data.
client.signOut();getState()
Get current state without making a request.
const state = client.getState();getSession()
Get the current session artifact (null if not authenticated).
const session = client.getSession();
if (session) {
// session.authCode - exchange this for tokens
// session.codeVerifier - for PKCE OAuth 2.1 flow
}onStateChange(listener)
Subscribe to state changes. Returns an unsubscribe function.
const unsub = client.onStateChange((state) => {
console.log('State changed:', state.status);
});
// Later
unsub();onSessionChange(listener)
Subscribe to session changes.
const unsub = client.onSessionChange((session) => {
if (session) {
console.log('Authenticated with code:', session.authCode);
}
});AuthState Helpers
Type Guards
Narrow the state type for TypeScript:
import {
isIdle,
isLoading,
isStep,
isCompleted,
isError,
} from '@hawcx/core';
if (isCompleted(state)) {
// state.authCode is available (type narrowed)
await exchangeToken(state.authCode, state.codeVerifier);
}Getters
Extract data from state without manual type checking:
import { getStepType, getMethods, getError } from '@hawcx/core';
// Get step type or null
const stepType = getStepType(state); // 'enter_code' | 'select_method' | null
// Get methods array (empty if not method selection)
const methods = getMethods(state); // Method[]
// Get error or null
const error = getError(state); // AuthError | nullStep Types
The server tells your UI what to render via step.type:
| Step Type | What to Render | User Action |
|-----------|----------------|-------------|
| select_method | List of auth methods | User picks one |
| enter_code | OTP input field | Enter code from email/SMS |
| enter_totp | TOTP input field | Enter code from authenticator app |
| setup_totp | QR code + input field | Scan QR, enter verification code |
| setup_sms | Phone number input | Enter phone for SMS enrollment |
| await_approval | Waiting indicator (+QR if provided) | Wait for approval on another device |
| redirect | Nothing (SDK handles redirect) | User completes OAuth flow |
| completed | Success message | Authentication complete |
| error | Error message | Show error, optionally retry |
Step Type Guards
import {
isSelectMethodStep,
isEnterCodeStep,
isEnterTotpStep,
isSetupTotpStep,
isSetupSmsStep,
isAwaitApprovalStep,
isCompletedStep,
isErrorStep,
} from '@hawcx/core';
if (state.status === 'step') {
if (isEnterCodeStep(state.step)) {
// Access: state.step.destination, state.step.codeLength, etc.
}
}Step Details
select_method
{
type: 'select_method';
phase: 'primary' | 'mfa' | 'enrollment';
methods: Array<{ name: string; label: string; icon?: string }>;
}enter_code
{
type: 'enter_code';
destination: string; // Masked (e.g., "s***@example.com")
codeLength: number;
codeFormat: 'numeric' | 'alphanumeric';
codeExpiresAt: string; // ISO 8601 timestamp
resendAt: string; // When resend becomes available
}setup_totp
{
type: 'setup_totp';
secret: string; // Base32-encoded TOTP secret
otpauthUrl: string; // For QR code generation
period: number; // Typically 30 seconds
}await_approval
{
type: 'await_approval';
qrData?: string; // QR code data for QR auth
expiresAt: string;
pollInterval: number; // Suggested poll interval in seconds
}Handling Errors
Errors include a category that indicates what action to take:
import { AuthErrorCategory, getError } from '@hawcx/core';
const error = getError(state);
if (error) {
switch (error.category) {
case AuthErrorCategory.RETRYABLE:
// User can try again (wrong code, etc.)
showError(error.message);
break;
case AuthErrorCategory.USER_ACTION:
// User needs to do something (resend code, etc.)
break;
case AuthErrorCategory.FATAL:
// Flow cannot continue, restart required
client.reset();
break;
}
}Complete Example
import {
createHawcxClient,
getStepType,
getMethods,
getError,
isCompleted,
} from '@hawcx/core';
const client = createHawcxClient({ configId: 'my-config' });
async function login(email: string) {
let state = await client.start('signin', email);
while (!isCompleted(state)) {
const error = getError(state);
if (error) {
console.error(error.message);
return null;
}
switch (getStepType(state)) {
case 'select_method':
const methods = getMethods(state);
const choice = await promptUser('Pick a method', methods);
state = await client.selectMethod(choice);
break;
case 'enter_code':
const code = await promptUser('Enter OTP');
state = await client.submitCode(code);
break;
case 'enter_totp':
case 'setup_totp':
const totp = await promptUser('Enter TOTP');
state = await client.submitTotp(totp);
break;
case 'setup_sms':
const phone = await promptUser('Enter phone');
state = await client.submitPhone(phone);
break;
case 'await_approval':
// Show waiting UI, SDK handles polling
await waitForApproval();
state = client.getState();
break;
default:
console.log('Unhandled step:', getStepType(state));
return null;
}
}
// Exchange auth code for tokens on your backend
return await exchangeForTokens(state.authCode, state.codeVerifier);
}Token Exchange
After authentication completes, exchange the authCode for tokens on your backend:
if (isCompleted(state)) {
const response = await fetch('/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: state.authCode,
code_verifier: state.codeVerifier, // For PKCE
}),
});
const { access_token, refresh_token } = await response.json();
}Device Trust
The SDK handles device trust automatically. When a user authenticates on a trusted device:
- Credentials are stored securely in IndexedDB
- On subsequent logins, the SDK loads and uses stored credentials
- Invalid credentials are automatically cleared with intelligent retry
To check if device credentials exist:
const hasCreds = await client.hasDeviceCredentials('[email protected]');TypeScript
All types are exported:
import type {
AuthState,
AuthStateStep,
AuthStateCompleted,
HawcxClient,
HawcxClientConfig,
SessionArtifact,
AuthStep,
Method,
AuthError,
} from '@hawcx/core';