npm package: @genzzi/oauth-server
Description: Drop-in TypeScript/JavaScript SDK for your OAuth 2.0 server. PKCE, token rotation, auto-refresh — all handled.
Latest version: 1.0.1

# oauth-client-sdk

Drop-in **TypeScript + JavaScript** SDK for your OAuth 2.0 server.  
Handles PKCE, auth-code exchange, token rotation, auto-refresh, revocation, and consent management — so your dev sites just call three methods instead of hand-rolling every HTTP call.

---

## Installation

```bash
npm install @genzzi/oauth-server
```

No runtime dependencies. Works in **Node.js 18+**, Deno, and modern browsers.

---

## Quick start (Express backend)

```ts
import express from 'express';
import session from 'express-session';
import { OAuthClient } from 'oauth-client-sdk';

const app = express();
app.use(express.json());
app.use(session({ secret: 'your-secret', resave: false, saveUninitialized: false }));

// ── One-time setup ─────────────────────────────────────────────────────────────
const oauth = new OAuthClient({
  baseUrl:      'https://api.yoursite.com/api/v1',
  clientId:     'your-client-id',
  clientSecret: 'your-client-secret',   // omit for public / PKCE clients
  redirectUri:  'http://localhost:3000/callback',
  scopes:       ['read:profile', 'write:posts'],
});

// ── Step 1: /login ─────────────────────────────────────────────────────────────
app.get('/login', async (req, res) => {
  const { url, state, codeVerifier } = await oauth.buildAuthorizationUrl();

  req.session.oauthState   = state;
  req.session.codeVerifier = codeVerifier; // only for public clients

  res.redirect(url);
});

// ── Step 2: /callback ──────────────────────────────────────────────────────────
app.get('/callback', async (req, res) => {
  const { code, state } = req.query as Record<string, string>;

  const tokens = await oauth.exchangeCode({
    code,
    state,
    storedState:        req.session.oauthState!,
    codeVerifier:       req.session.codeVerifier,
    oauthSessionCookie: req.cookies.oauth_session,
  });

  req.session.tokens = tokens;
  res.redirect('/dashboard');
});

// ── Step 3: protected route — tokens auto-refresh ──────────────────────────────
app.get('/dashboard', async (req, res) => {
  if (!req.session.tokens) return res.redirect('/login');

  const profile = await oauth.withAutoRefresh(
    req.session.tokens,
    (accessToken) => fetch('https://api.yoursite.com/api/v1/me', {
      headers: { Authorization: `Bearer ${accessToken}` },
    }).then(r => r.json()),
  );

  res.json(profile);
});

// ── Logout ─────────────────────────────────────────────────────────────────────
app.get('/logout', async (req, res) => {
  if (req.session.tokens) {
    await oauth.revokeToken(req.session.tokens.access_token, 'access_token');
    await oauth.revokeToken(req.session.tokens.refresh_token, 'refresh_token');
  }
  req.session.destroy(() => res.redirect('/'));
});
```

---

## Express middleware

Attach a shared `OAuthClient` to every request as `req.oauth`:

```ts
import { createOAuthMiddleware } from 'oauth-client-sdk/express';

app.use(createOAuthMiddleware({
  baseUrl:      'https://api.yoursite.com/api/v1',
  clientId:     'abc123',
  clientSecret: 'secret',
  redirectUri:  'http://localhost:3000/callback',
  scopes:       ['read:profile'],
}));

app.get('/me', async (req, res) => {
  const data = await req.oauth.withAutoRefresh(
    req.session.tokens,
    (token) => myApi.getUser(token),
  );
  res.json(data);
});
```

---

## Plain JavaScript (CommonJS)

```js
const { OAuthClient } = require('oauth-client-sdk');

const oauth = new OAuthClient({ ... });
```

## Plain JavaScript (ESM)

```js
import { OAuthClient } from 'oauth-client-sdk';

const oauth = new OAuthClient({ ... });
```

---

## API Reference

### `new OAuthClient(config)`

| Field | Type | Required | Description |
|---|---|---|---|
| `baseUrl` | `string` | ✅ | Your server base URL e.g. `https://api.site.com/api/v1` |
| `clientId` | `string` | ✅ | OAuth `client_id` from registration |
| `clientSecret` | `string` | — | Omit for public (PKCE-only) clients |
| `redirectUri` | `string` | ✅ | Exact URI registered on the server |
| `scopes` | `string[]` | ✅ | Scopes to request |
| `extraHeaders` | `Record<string,string>` | — | Extra headers on every request |

---

### `oauth.buildAuthorizationUrl()` → `Promise<AuthorizationUrlResult>`

Validates params with the server and returns the URL to redirect the user to.  
For public clients, generates PKCE automatically.

**Always persist `state` (and `codeVerifier`) in your session before redirecting.**

```ts
const { url, state, codeVerifier } = await oauth.buildAuthorizationUrl();
req.session.oauthState   = state;
req.session.codeVerifier = codeVerifier; // public clients only
res.redirect(url);
```

---

### `oauth.exchangeCode(params)` → `Promise<TokenSet>`

Exchanges the auth code for access + refresh tokens.  
Performs CSRF state check before the network call.

| Param | Description |
|---|---|
| `code` | `?code=` from the callback query string |
| `state` | `?state=` from the callback query string |
| `storedState` | The `state` you saved in your session |
| `codeVerifier` | The verifier from `buildAuthorizationUrl` (public clients) |
| `oauthSessionCookie` | Value of the `oauth_session` cookie from the browser |

---

### `oauth.refreshTokens(refreshToken)` → `Promise<TokenSet>`

Rotates both tokens. The old refresh token is invalidated server-side.

---

### `oauth.withAutoRefresh(tokenStore, fn)` → `Promise<T>`

Wraps any API call. Refreshes proactively 60 s before expiry, retries once on 401.  
Mutates `tokenStore` in-place with fresh values.

```ts
const data = await oauth.withAutoRefresh(
  req.session.tokens,
  (accessToken) => myApi.call(accessToken),
);
```

---

### `oauth.revokeToken(token, hint?)` → `Promise<void>`

RFC 7009 — always resolves, never throws.

```ts
await oauth.revokeToken(tokens.access_token, 'access_token');
await oauth.revokeToken(tokens.refresh_token, 'refresh_token');
```

---

### `oauth.introspect(token)` → `Promise<IntrospectionResult>`

Returns `{ active: true, sub, deviceId }` or `{ active: false }`. Never throws.

---

### `oauth.listConsents(accessTokenCookie)` → `Promise<ConsentEntry[]>`

Lists all apps the logged-in user has approved.

---

### `oauth.revokeConsent(clientId, accessTokenCookie)` → `Promise<void>`

Disconnects an app and invalidates all its tokens.

---

### `oauth.registerClient(params, developerCookie)` → `Promise<RegisteredClient>`

Registers a new OAuth app. `clientSecret` is returned **once** — store it immediately.

---

## Error handling

```ts
import { OAuthError, OAuthStateMismatchError } from 'oauth-client-sdk';

try {
  const tokens = await oauth.exchangeCode({ ... });
} catch (err) {
  if (err instanceof OAuthStateMismatchError) {
    // CSRF detected — restart the flow
  }
  if (err instanceof OAuthError) {
    console.error(err.status, err.message, err.body);
  }
}
```

| Error class | When thrown |
|---|---|
| `OAuthError` | Server returned non-2xx |
| `OAuthStateMismatchError` | `state !== storedState` in `exchangeCode` |
| `OAuthSessionMissingError` | `oauthSessionCookie` is empty in `exchangeCode` |

---

## Static PKCE helpers

Available standalone if you need them outside the SDK:

```ts
import { generateCodeVerifier, generateCodeChallenge, generateState } from 'oauth-client-sdk';

const verifier   = await generateCodeVerifier();
const challenge  = await generateCodeChallenge(verifier);
const state      = await generateState();
```

Also accessible as `OAuthClient.generateCodeVerifier()` etc.

---

## Build the library yourself

```bash
npm install
npm run build       # outputs to dist/
npm run typecheck   # strict TS check
```

---

## License

MIT