# Auth pools

Each project can have one pool. End users sign up and log in, Harakumo stores them (passwords hashed with PBKDF2) and returns a signed token your app checks on each request.

Enabling a pool returns three values. The client secret (`as_…`) lets your server act for this pool only; it is shown once and can be rotated. The JWT secret (`js_…`) verifies tokens; developers can show it again. The client id (`ap_…`) is publishable and keys the browser endpoints.

## Two ways to call it

| From | Endpoint | Credential |
| --- | --- | --- |
| Your server | `POST /api/auth-pools/:poolId` with an `action` | Bearer client secret (as_…), or an API key |
| A browser or mobile app | `POST /api/auth-pools/public/:clientId/:action` | None — the client id is in the URL. Off until you turn it on. |

## Enable a pool

**curl**

```bash
curl -X POST https://harakumo.com/api/projects/<projectId>/auth \
  -H "Authorization: Bearer $HARAKUMO_API_KEY"
# 201 → { "pool": {…}, "clientId": "ap_…", "clientSecret": "as_…", "jwtSecret": "js_…",
#         "serverEndpoint", "browserEndpoint", "verifyEndpoint", "algorithm": "HS256" }
```

> Store clientSecret and jwtSecret as environment variables of the project (for example HK_CLIENT_SECRET and HK_JWT_SECRET), with the pool id, client id and issuer. The issuer is `https://harakumo.com/auth/<project-slug>`.

## Sign-up and login from your server

Every call names its action: signup, login, send-verification, verify-email, forgot-password or reset-password. Your server keeps the returned token in its own cookie and verifies it on each request. The example uses express and jose (`npm install express jose`).

**Express**

```js
import express from 'express';
import { jwtVerify } from 'jose';

const POOL = 'https://harakumo.com/api/auth-pools/' + process.env.HK_POOL_ID;
const secret = new TextEncoder().encode(process.env.HK_JWT_SECRET);

async function pool(body) {
  const r = await fetch(POOL, {
    method: 'POST',
    headers: { authorization: 'Bearer ' + process.env.HK_CLIENT_SECRET, 'content-type': 'application/json' },
    body: JSON.stringify(body),
  });
  const data = await r.json();
  if (!r.ok) throw Object.assign(new Error(data.error), { status: r.status });
  return data;   // { user, token, expiresIn }
}

const app = express();
app.use(express.json());

app.post('/signup', async (req, res) => {
  try {
    const { token } = await pool({ action: 'signup', email: req.body.email, password: req.body.password });
    res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 7 * 864e5 }).json({ ok: true });
  } catch (err) {
    res.status(err.status || 500).json({ error: err.message });   // 409 when the email already has an account
  }
});

app.post('/login', async (req, res) => {
  try {
    const { token } = await pool({ action: 'login', email: req.body.email, password: req.body.password });
    res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 7 * 864e5 }).json({ ok: true });
  } catch (err) {
    res.status(err.status === 429 ? 429 : 401).json({ error: err.message });
  }
});

// Verify locally on every request: no call to Harakumo.
async function requireUser(req, res, next) {
  const token = (req.headers.cookie || '').match(/(?:^|; )session=([^;]+)/)?.[1];
  try {
    const { payload } = await jwtVerify(token || '', secret, {
      algorithms: ['HS256'], issuer: process.env.HK_ISSUER, audience: process.env.HK_CLIENT_ID,
    });
    req.user = payload;   // sub, email, name, email_verified …
    next();
  } catch {
    res.status(401).json({ error: 'Sign in first' });
  }
}

app.get('/me', requireUser, (req, res) => res.json({ id: req.user.sub, email: req.user.email }));
app.listen(process.env.PORT || 3000);
```

## Token claims

| Claim | Value |
| --- | --- |
| iss | The pool's issuer, `https://harakumo.com/auth/<project-slug>` |
| aud | The pool's client id |
| sub | The end user's id |
| email, name | As signed up |
| email_verified | true once they entered a verification code |
| pool | The pool id |
| tv | Token version: moves on when the user is reset or signed out everywhere |
| iat, exp | Issued and expiry; tokens last 7 days (HS256) |

> Local verification cannot see a revocation until the token expires. `POST /api/auth-pools/:id/verify { token }` also refuses tokens of deleted users and users signed out everywhere (reason "revoked"); call it where that matters.

## Sign-in from the browser or a mobile app

1. Turn browser access on: `PATCH /api/auth-pools/:id/settings { publicEnabled: true }`. Letting anyone create an account is a separate switch, `publicSignup: true`.
2. The project's own addresses and verified custom domains are allowed automatically. Add other origins with `allowedOrigins` (https only; http only for localhost; up to 20). A request from any other Origin is refused before anything runs.
3. Call the public endpoints with the client id. `GET …/session` with `Authorization: Bearer <token>` answers who is signed in.

**Browser**

```js
const AUTH = 'https://harakumo.com/api/auth-pools/public/ap_…';

const r = await fetch(AUTH + '/login', {
  method: 'POST', headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ email, password }),
});
const { token, user, error } = await r.json();

const me = await (await fetch(AUTH + '/session', { headers: { authorization: 'Bearer ' + token } })).json();
```

> Browser limits: sign-up 10 per IP an hour and 600 per pool an hour; login 30 per IP per 10 minutes; codes 20 per IP an hour, and 50 an hour and 300 a day per pool (see below).

## Email verification and password reset

1. `send-verification { email }` emails a 6-digit code; `verify-email { email, code }` marks the address verified and returns a fresh token.
2. `forgot-password { email }` emails a reset code; `reset-password { email, code, password }` sets the new password, signs the user out everywhere else and returns a token.

- Codes last 15 minutes and allow 5 tries; at most 5 codes an hour per address.
- They are sent under your app's sender name and count toward the workspace's email quota. If the app has no sender name recipients may see (see Mail → Sender names), code requests answer 409 `mail_not_ready`.
- Browser answers never reveal whether an email has an account.

## Limits on codes asked for from the browser

Anyone who has your client id can ask the public endpoints to email a code, so those codes have limits of their own, and a stranger cannot spend the mail your app needs for its own password resets:

- Per pool: at most 50 codes an hour and 300 a day. Only codes actually mailed count: a request for an address with no account sends nothing and uses none.
- Per workspace: codes asked for from the browser, across all its pools, may use at most 20% of the 30-day email quota (20 on hobby, 1,000 on pro, 20,000 on enterprise). They still count toward the quota like any email.
- Past either limit, `send-verification` and `forgot-password` answer 429 with code `code_limit` and a Retry-After header, whatever the address, so the answer reveals nothing about who has an account. The per-IP and per-address limits answer 429 `rate_limited`.
- Codes your server asks for with the client secret (or an API key) are not held to these limits, only to the 5 an hour per address, so resets your own server sends keep working.

## Managing users and secrets

- `GET /api/auth-pools/:id?q=&before=&limit=` lists users (up to 200 a page).
- `PATCH /api/auth-pools/:id/users/:userId` with action reset-password (with password), send-password-reset, revoke-sessions or unlock. `DELETE` removes the user; their tokens stop verifying.
- `GET /api/auth-pools/:id/secret` shows the JWT secret again (developer and up, audited).
- `POST /api/auth-pools/:id/secret { which: "jwt" }` rotates it, keeping the old one valid for 24 hours (or `keepPrevious: false` to revoke at once). `{ which: "client" }` replaces the client secret immediately. Rotating needs an admin.

## Limits and what is not available yet

- Passwords 8–256 characters. 10 wrong passwords lock the account for 15 minutes (429 with Retry-After).
- Codes asked for from the browser: 50 an hour and 300 a day per pool, and at most 20% of the workspace's email quota (429 `code_limit`).
- No social login, no end-user MFA, no hosted sign-in page, no refresh tokens and no RS256/JWKS yet.
- Pools per plan: 1 hobby, 10 pro, 100 enterprise; end users per pool are not capped.
- Verifying tokens locally scales with your app. Sign-ups and logins run on the shared platform and suit apps with thousands of active users.

## Reference

**SDK**

```js
// On your server, the client secret works as the SDK key — for this pool's sign-in actions only.
const pool = new Harakumo({ apiKey: process.env.HK_CLIENT_SECRET });
const { token } = await pool.auth.signup(poolId, { email: 'user@yourapp.com', password: 'correct-horse-9' });
const { token: t2 } = await pool.auth.login(poolId, { email: 'user@yourapp.com', password: 'correct-horse-9' });
const { valid, claims, reason } = await pool.auth.verifyToken(poolId, t2);

// Managing the pool needs an API key:
const { pool: p, clientId, clientSecret, jwtSecret } = await hk.auth.enable(project.id);
```

**REST**

```http
POST   /api/projects/:id/auth                     # enable → clientId, clientSecret, jwtSecret, endpoints
POST   /api/auth-pools/:id        { "action": "signup"|"login"|"send-verification"|"verify-email"|"forgot-password"|"reset-password", "email", "password"?, "code"?, "name"? }
POST   /api/auth-pools/:id/verify { "token": "…" }   → { valid, claims, user } | { valid: false, reason }
GET    /api/auth-pools/:id/settings · PATCH { "publicEnabled"?, "publicSignup"?, "allowedOrigins"? }
GET    /api/auth-pools/:id/secret · POST { "which": "jwt"|"client", "keepPrevious"? }
GET    /api/auth-pools/:id?q=&before=&limit=     # users
PATCH  /api/auth-pools/:id/users/:userId         { "action": "reset-password"|"send-password-reset"|"revoke-sessions"|"unlock" }
DELETE /api/auth-pools/:id/users/:userId
DELETE /api/auth-pools/:id                       # disable: removes the pool and every user

POST   /api/auth-pools/public/:clientId/signup | login | send-verification | verify-email | forgot-password | reset-password
GET    /api/auth-pools/public/:clientId/session  Authorization: Bearer <token>
```
