# @bkper/web-auth

> Web authentication SDK for Bkper — OAuth flows, token management, and session handling.

[![npm](https://img.shields.io/npm/v/@bkper/web-auth?color=%235889e4)](https://www.npmjs.com/package/@bkper/web-auth) [![GitHub](https://img.shields.io/badge/bkper%2Fbkper--web--sdks-blue?logo=github)](https://github.com/bkper/bkper-web-sdks)

# @bkper/web-auth

OAuth authentication SDK for apps on the [Bkper Platform](https://bkper.com/docs/build/apps/overview) (`*.bkper.app` subdomains).

## Quick Start

```typescript
import { BkperAuth } from '@bkper/web-auth';

// Initialize client with callbacks
const auth = new BkperAuth({
    onLoginSuccess: () => {
        console.log('User authenticated!');
        loadUserData();
    },
    onLoginRequired: () => {
        console.log('Please sign in');
        showLoginButton();
    },
});

// Initialize authentication flow on app load
await auth.init();

// Make an authenticated request with automatic token refresh and one retry
const response = await auth.authenticatedFetch('/data');
```

## Authenticated Requests

`authenticatedFetch()` implements the standard Fetch API contract. It adds the current bearer token to a request. If the response is `401`, it refreshes the token and retries exactly once. Other response statuses are returned unchanged.

```typescript
const response = await auth.authenticatedFetch('/data', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ value: 42 }),
});
```

The method can also be supplied to any HTTP client that accepts a Fetch-compatible function:

```typescript
const fetchWithAuth = auth.authenticatedFetch.bind(auth);
```

Call `init()` before the first authenticated request. If no token is available, or the session cannot be refreshed, `onLoginRequired` is called and the request rejects with an authentication-required error. If the retried request also returns `401`, that response is returned without another retry. Concurrent refresh calls share one refresh request.

To prevent accidental token disclosure, authenticated requests are restricted to:

- HTTPS origins on `bkper.app` or its subdomains
- The current `localhost` or `127.0.0.1` origin during local development

Request paths are not restricted.

### Using with bkper-js

`@bkper/web-auth` does not depend on `bkper-js`, but they can be connected through the client configuration. Provide the current token for each request and refresh it when the Bkper API reports an expired login:

```typescript
import { Bkper } from 'bkper-js';

const bkper = new Bkper({
    oauthTokenProvider: async () => auth.getAccessToken(),
    requestRetryHandler: async (status, _error, attempt) => {
        if (status === 403 && attempt === 1) {
            await auth.refresh();
        }
    },
});
```

`bkper-js` owns its request and retry lifecycle. `@bkper/web-auth` remains responsible only for the current access token and session refresh.

## What's Included

-   OAuth authentication SDK for apps on `*.bkper.app` subdomains
-   Callback-based API for authentication events
-   OAuth flow with in-memory token management
-   Single-flight token refresh mechanism
-   Authenticated Fetch API with one-time refresh and retry
-   TypeScript support with full type definitions

## How It Works

**Session Persistence:**

-   Access tokens are stored in-memory (cleared on page refresh)
-   Sessions persist via HTTP-only cookies scoped to the `.bkper.app` domain
-   Call `init()` on app load to restore an access token from the session
-   Protected resources still require `Authorization: Bearer <token>`; session cookies only restore client auth state

> **Note:** This SDK only works for apps hosted on `*.bkper.app` subdomains. Applications on other domains must provide a valid access token through their own authentication mechanism.

**Security:**

-   HTTP-only cookies protect refresh tokens from XSS
-   In-memory access tokens minimize exposure

## TypeScript Support

This package is written in TypeScript and provides full type definitions out of the box. All public APIs are fully typed, including callbacks and configuration options.

```typescript
import { BkperAuth, BkperAuthConfig } from '@bkper/web-auth';

const config: BkperAuthConfig = {
    onLoginSuccess: () => console.log('Authenticated'),
    onError: error => console.error('Auth error:', error),
};

const auth = new BkperAuth(config);
```

## Browser Compatibility

This package requires a modern browser with support for:

-   [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#browser_compatibility) for HTTP requests
-   [Location API](https://developer.mozilla.org/en-US/docs/Web/API/Location) for login/logout redirects

The app must be deployed to a `*.bkper.app` subdomain for session-cookie token restoration to work.

## Classes

### BkperAuth

OAuth authentication client for the Bkper API.

Provides framework-agnostic authentication with callback-based event handling.
Access tokens are stored in-memory; sessions persist via HTTP-only cookies.

```typescript
// Initialize authentication client
const auth = new BkperAuth({
  onLoginSuccess: () => loadUserData(),
  onLoginRequired: () => showLoginButton()
});

// Restore session on app load
await auth.init();
```

**Constructor:** `new BkperAuth(config?: BkperAuthConfig)`

Creates a new BkperAuth instance.

```typescript
// Simple usage with defaults
const auth = new BkperAuth();

// With callbacks
const auth = new BkperAuth({
  onLoginSuccess: () => console.log('Logged in!'),
  onLoginRequired: () => showLoginDialog(),
  onError: (error) => console.error(error)
});
```

**Methods:**

- `authenticatedFetch(input: RequestInfo | URL, init?: RequestInit)` → `Promise<Response>` — Performs an authenticated request and retries it once after refreshing an
expired or invalid access token.
- `getAccessToken()` → `string | undefined` — Gets the current access token.
- `init()` → `Promise<void>` — Initializes the authentication state by attempting to refresh the access token.
- `login()` → `void` — Redirects the user to the login page.
- `logout()` → `void` — Logs out the user and redirects to the logout page.
- `refresh()` → `Promise<void>` — Refreshes the access token using the current session.

**authenticatedFetch**

Concurrent refresh calls share the same refresh request. A second 401
response is returned without another retry.

Call `init()` before the first request. Bearer tokens are sent only to
HTTPS Bkper origins or the current local development origin. Request
paths are not restricted.

**getAccessToken**

```typescript
const tokenProvider = async () => auth.getAccessToken();
```

The access token if authenticated, undefined otherwise

Use

`authenticatedFetch()`

for Fetch API requests. This getter is
available for HTTP clients that accept an access-token provider.

**init**

Call this method when your app loads to restore the user's session.
Triggers `onLoginSuccess` if a valid session exists, or `onLoginRequired` if login is needed.

**login**

The user will be redirected to the authentication service to complete the login flow.
After successful login, they will be redirected back to the current page.

```typescript
// Trigger login when user clicks a button
loginButton.addEventListener('click', () => {
  auth.login();
});
```

**logout**

Triggers the `onLogout` callback before redirecting.
The user's session will be terminated.

```typescript
// Logout when user clicks logout button
logoutButton.addEventListener('click', () => {
  auth.logout();
});
```

**refresh**

Concurrent calls share one refresh request. Triggers `onTokenRefresh`
if successful and throws if the refresh request fails.

`authenticatedFetch()` calls this method automatically after a 401.
Consumers can also call it explicitly when they need a new token.

```typescript
await auth.refresh();
const token = auth.getAccessToken();
```

## Interfaces

### BkperAuthConfig

Configuration options for the BkperAuth class.

**Properties:**

- `baseUrl?`: `string` — Override the authentication service base URL.
- `getAdditionalAuthParams?`: `() => Record<string, string>` — Provide additional parameters to send to the authentication service.
- `onError?`: `(error: unknown) => void` — Called when an error occurs during authentication.
- `onLoginRequired?`: `() => void` — Called when login is required (user needs to sign in).
- `onLoginSuccess?`: `() => void` — Called when login succeeds (user is authenticated).
- `onLogout?`: `() => void` — Called when the user logs out.
- `onTokenRefresh?`: `(token: string) => void` — Called when the access token is refreshed.

**baseUrl**

Most users don't need this. The default production URL works out of the box.

Use cases:
- Testing: Point to a mock authentication service for integration tests
- Development: Use a local mock server

```typescript
// Testing with mock server
const auth = new BkperAuth({
  baseUrl: 'http://localhost:3000/mock-auth'
});
```

**getAdditionalAuthParams**

Useful for custom authentication flows or passing additional context
to your authentication implementation.

```typescript
// Custom authentication context
const auth = new BkperAuth({
  getAdditionalAuthParams: () => {
    const token = new URLSearchParams(location.search).get('custom-token');
    return token ? { customToken: token } : {};
  }
});
```

