# @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

```bash
npm install @hawcx/core
```

## Quick Start

```typescript
import { createHawcxClient } from '@hawcx/core';

const client = createHawcxClient({
  configId: 'your-config-id',
});

// Start signin flow
const state = await client.start('signin', 'user@example.com');

// 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
                   ↓
                 error
```

Every method returns `AuthState`:

```typescript
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

```typescript
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.

```typescript
// Signin
await client.start('signin', 'user@example.com');

// Signup
await client.start('signup', 'newuser@example.com');

// Account management (step-up auth for settings changes)
await client.start('account_manage', 'user@example.com');
```

### `selectMethod(methodId)`

Choose an auth method when `step.type === 'select_method'`.

```typescript
await client.selectMethod('email_otp');
```

### `submitCode(code)`

Submit OTP code for `enter_code` steps.

```typescript
await client.submitCode('123456');
```

### `submitTotp(code)`

Submit TOTP code for `enter_totp` or `setup_totp` steps.

```typescript
await client.submitTotp('123456');
```

### `submitPhone(phone)`

Submit phone number for `setup_sms` steps (SMS enrollment).

```typescript
await client.submitPhone('+14155551234');
```

### `resend()`

Request a new OTP code.

```typescript
await client.resend();
```

### `cancel()`

Cancel the current flow.

```typescript
await client.cancel();
```

### `reset()`

Reset to idle state and clear session data.

```typescript
client.reset();
```

### `signOut()`

Sign out and clear all session data.

```typescript
client.signOut();
```

### `getState()`

Get current state without making a request.

```typescript
const state = client.getState();
```

### `getSession()`

Get the current session artifact (null if not authenticated).

```typescript
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.

```typescript
const unsub = client.onStateChange((state) => {
  console.log('State changed:', state.status);
});

// Later
unsub();
```

### `onSessionChange(listener)`

Subscribe to session changes.

```typescript
const unsub = client.onSessionChange((session) => {
  if (session) {
    console.log('Authenticated with code:', session.authCode);
  }
});
```

## AuthState Helpers

### Type Guards

Narrow the state type for TypeScript:

```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:

```typescript
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 | null
```

## Step 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

```typescript
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`**
```typescript
{
  type: 'select_method';
  phase: 'primary' | 'mfa' | 'enrollment';
  methods: Array<{ name: string; label: string; icon?: string }>;
}
```

**`enter_code`**
```typescript
{
  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`**
```typescript
{
  type: 'setup_totp';
  secret: string;           // Base32-encoded TOTP secret
  otpauthUrl: string;       // For QR code generation
  period: number;           // Typically 30 seconds
}
```

**`await_approval`**
```typescript
{
  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:

```typescript
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

```typescript
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:

```typescript
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:

1. Credentials are stored securely in IndexedDB
2. On subsequent logins, the SDK loads and uses stored credentials
3. Invalid credentials are automatically cleared with intelligent retry

To check if device credentials exist:

```typescript
const hasCreds = await client.hasDeviceCredentials('user@example.com');
```

## TypeScript

All types are exported:

```typescript
import type {
  AuthState,
  AuthStateStep,
  AuthStateCompleted,
  HawcxClient,
  HawcxClientConfig,
  SessionArtifact,
  AuthStep,
  Method,
  AuthError,
} from '@hawcx/core';
```
