Skip to main content

OAuth Client

The core OAuth client handles DPoP key provisioning, session registration, and session restoration against a HappyView instance. It's platform-agnostic — you provide a CryptoAdapter and optional StorageAdapter for your environment.

If you're building a browser app, use the Browser Client instead. It wraps this package with Web Crypto, localStorage, and a complete OAuth redirect flow.

Installation

npm install @happyview/oauth-client

Setup

import { HappyViewOAuthClient } from "@happyview/oauth-client";

const client = new HappyViewOAuthClient({
instanceUrl: "https://happyview.example.com",
clientKey: "hvc_your_client_key",
clientSecret: "hvs_your_secret", // optional, for confidential clients
crypto: myCryptoAdapter,
storage: myStorageAdapter, // optional, defaults to in-memory
});

The clientSecret parameter makes this a confidential client. Omit it for public clients (browser apps), which use PKCE instead. See Authentication — API clients for details.

DPoP key provisioning

Request a DPoP keypair from the HappyView instance. This is the first step of the DPoP key provisioning flow.

const { provisionId, dpopKey, pkceVerifier } =
await client.provisionDpopKey();

For public clients, pkceVerifier is included and must be passed back when registering the session. For confidential clients it will be undefined.

Use the returned dpopKey (a private JWK) as your DPoP keypair during your atproto OAuth flow with the user's PDS.

Session registration

After completing OAuth authorization with the user's PDS, register the session with HappyView:

const session = await client.registerSession({
provisionId,
pkceVerifier, // required for public clients
did: "did:plc:abc123",
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
scopes: "atproto",
pdsUrl: "https://bsky.social",
issuer: tokens.iss,
dpopKey,
});

The returned HappyViewSession is ready to make authenticated requests. The session data is also persisted to the StorageAdapter for later restoration.

Making authenticated requests

HappyViewSession.fetchHandler works like fetch but automatically attaches DPoP proof, authorization, and client key headers:

// Relative path — prepends the HappyView instance URL
const response = await session.fetchHandler(
"/xrpc/com.example.getStuff?limit=10",
{ method: "GET" },
);

// Absolute URL — used as-is
const response = await session.fetchHandler(
"https://other-service.example.com/xrpc/test.method",
{ method: "GET" },
);

Session restoration

Restore a previously stored session without re-authenticating:

// Restore the last active session
const session = await client.restore();

// Restore a specific user's session
const session = await client.restoreSession("did:plc:abc123");

Returns null if no stored session is found.

Logout

await client.deleteSession("did:plc:abc123");

This deletes the session from both HappyView and local storage.

Adapters

CryptoAdapter

Implement this interface for your platform's cryptographic primitives:

interface CryptoAdapter {
generatePkceVerifier(): Promise<string>;
computePkceChallenge(verifier: string): Promise<string>;
signEs256(privateKey: JsonWebKey, payload: Uint8Array): Promise<Uint8Array>;
sha256(data: Uint8Array): Promise<Uint8Array>;
getRandomValues(length: number): Uint8Array;
}

StorageAdapter

Implement this interface to persist sessions:

interface StorageAdapter {
get(key: string): Promise<string | null>;
set(key: string, value: string): Promise<void>;
delete(key: string): Promise<void>;
}

If no StorageAdapter is provided, sessions are stored in memory and won't survive page reloads or process restarts.

note

The built-in MemoryStorage is exported for testing. In production, always provide a persistent storage adapter.

Error handling

All errors extend HappyViewError:

ErrorWhen
ApiErrorHappyView API returned a non-OK response (has status and body)
AuthenticationErrorAuthentication failed (default status 401)
InvalidStateErrorMissing or invalid OAuth state
TokenExchangeErrorToken exchange with the PDS failed (has status and body)
ResolutionErrorHandle or DID resolution failed
import { ApiError } from "@happyview/oauth-client";

try {
await client.registerSession(params);
} catch (err) {
if (err instanceof ApiError) {
console.error(`API error ${err.status}:`, err.body);
}
}