# JavaScript SDK

> Every class, function and option of @9in-io/sdk and @9in-io/sdk/server.

Source: https://9in.io/docs/reference/sdk/

`@9in-io/sdk` has no dependencies and runs in browsers, Node 18+, Cloudflare Workers, Deno and Bun. It ships TypeScript types.

```bash
npm install @9in-io/sdk
```

| Import | For |
|---|---|
| `@9in-io/sdk` | Signing users in from browser apps: `NineAuth` |
| `@9in-io/sdk/server` | Back ends: token verification, the Management API, webhooks, the adapter |

Every failure is a `NineError` with a `code` (and `status` for HTTP errors). Issuers must use `https://`; plain http is accepted for `localhost`, `*.localhost` and loopback addresses, or anywhere with `allowInsecureIssuer: true` (development only).

## NineAuth (`@9in-io/sdk`)

```ts
const auth = new NineAuth(options);
```

| Option | Type | |
|---|---|---|
| `issuer` | string | Your tenant's issuer, e.g. `https://acme.9in.io` |
| `clientId` | string | |
| `redirectUri` | string | A registered callback URL |
| `scope` | string | Default `"openid profile email offline_access"` |
| `audience` | string | API identifier for access tokens |
| `storage` | `StorageLike` | Where tokens are kept (default `localStorage`, else memory) |
| `transientStorage` | `StorageLike` | Where the PKCE verifier and state wait during the redirect (default `sessionStorage`) |
| `locale` | string | Language of the hosted pages (`ui_locales`) |
| `navigate` | `(url) => void` | Default `window.location.assign` |
| `fetch` | `typeof fetch` | Custom fetch (tests, React Native) |
| `allowInsecureIssuer` | boolean | |

`StorageLike` is `{ getItem(key), setItem(key, value), removeItem(key) }`.

| Method | Returns | |
|---|---|---|
| `login(options?)` | `Promise<void>` | Redirects to the hosted sign-in (authorization code + PKCE) |
| `signup(options?)` | `Promise<void>` | Opens the sign-up form (`prompt=create`) |
| `buildLoginUrl(options?)` | `Promise<string>` | The URL `login()` would navigate to |
| `handleRedirect(url?)` | `Promise<{ user: User; returnTo?: string }>` | On the callback page: checks `state` and `iss`, exchanges the code, stores tokens, cleans the address bar |
| `isAuthenticated()` | boolean | A usable access token or a refresh token is stored |
| `getUser()` | `Promise<User \| null>` | Claims from the ID token |
| `fetchUserInfo()` | `Promise<User>` | Fresh claims from `/oauth/userinfo` |
| `getAccessToken()` | `Promise<string>` | Refreshed (with rotation) when it expires within a minute; concurrent callers share one refresh. Throws `login_required` when signed out |
| `getTokens()` | `TokenSet \| null` | `{ accessToken, idToken, refreshToken, expiresAt, scope }` |
| `fetch(input, init?)` | `Promise<Response>` | `fetch` with `Authorization: Bearer` |
| `logout(options?)` | `Promise<void>` | Revokes the refresh token, clears tokens and ends the 9in session. `{ returnTo }` (an allowed logout URL); `{ local: true }` only clears local tokens |

`LoginOptions`: `prompt` (`"login" \| "consent" \| "none" \| "select_account"`), `loginHint`, `screen` (`"login" \| "signup"`), `returnTo` (handed back by `handleRedirect`), `params` (extra authorization parameters).

## verifyAccessToken (`@9in-io/sdk/server`)

```ts
verifyAccessToken(token: string | null | undefined, options: VerifyOptions): Promise<AccessTokenClaims>
```

Verifies a JWT access token locally: `typ: at+jwt`, the signature with the key named by `kid` (JWKS cached, refetched once for an unknown `kid`) using that key's own algorithm, `iss`, `aud`, `exp`, and scopes.

| Option | |
|---|---|
| `issuer` | Exactly the token's `iss` |
| `audience` | string or string[]: the API identifier(s) accepted |
| `scope` | string (space-separated) or string[]: all required |
| `secret` | The API's HMAC secret, for HS256/384/512 tokens |
| `clockTolerance` | Seconds, default 30 |
| `fetch`, `allowInsecureIssuer` | |

Throws `NineError` with `invalid_token` or `insufficient_scope`. `AccessTokenClaims`: `iss`, `sub`, `aud`, `exp`, `iat`, `client_id`, `scope`, `jti?`, `sid?`, `auth_time?`.

## createTokenVerifier

```ts
const verifier = createTokenVerifier(options: TokenVerifierOptions): TokenVerifier
```

`VerifyOptions` (without `scope`) plus:

| Option | |
|---|---|
| `mode` | `"hybrid"` (default), `"local"`, `"introspect"`. Hybrid and introspect need `clientId` and `clientSecret` (`invalid_options` otherwise) |
| `clientId`, `clientSecret` | A confidential application, for introspection |
| `cacheTtl` | Seconds a hybrid answer is trusted without news (default 300) |
| `cacheSize` | Most tokens cached (default 10 000) |

| Method | |
|---|---|
| `verify(token, { scope? })` | Claims, or throws `NineError`. Opaque tokens are always introspected |
| `handleEvent(event)` | Drops cached answers a webhook event makes stale; returns how many |
| `clear()` | Forgets every cached answer |

## ManagementClient

```ts
const nine = new ManagementClient({ issuer, clientId, clientSecret, fetch?, allowInsecureIssuer? });
```

Gets client-credentials tokens itself, one per management scope, and caches them.

| Method | API call |
|---|---|
| `listUsers({ query?, cursor?, limit? })` | `GET /users` → `{ users, nextCursor }` |
| `getUser(id)` | `GET /users/{id}` |
| `findUserByEmail(email)` | `GET /users/lookup?email=` → `{ user }` |
| `createUser(input)` | `POST /users` → `{ user }` |
| `updateUser(id, input)` | `PATCH /users/{id}` → `{ user }` |
| `deleteUser(id)` | `DELETE /users/{id}` |
| `blockUser(id, blocked = true)` | `PATCH /users/{id}` with `{ blocked }` |
| `createLoginLink(id, { returnTo?, ttlSeconds? })` | `POST /users/{id}/login-link` → `{ url, expires_at }` |
| `revokeUserSessions(id)` | `DELETE /users/{id}/sessions` → `{ revoked }` |

Errors are `NineError` with the API's `error` as `code` and the HTTP `status`. For endpoints without a method (import, sessions), call the [Management API](https://9in.io/docs/reference/management-api/) directly.

## verifyWebhook

```ts
verifyWebhook(secret: string | string[], request: Request, options?: { toleranceSeconds?: number; path?: string }): Promise<WebhookEvent>
```

Checks `9in-signature` against the raw body and returns the parsed event, or throws `NineError("invalid_signature")`. Several secrets are accepted during rotation. `path` overrides the request path when a proxy rewrote it. It reads the body: pass `request.clone()` to read it again.

`WebhookEvent`: `{ id, type, createdAt, tenant, environment, data }`, with `data.userId` and `data.user` for user events.

## processEventOnce

```ts
processEventOnce<T>(store: EventStore, eventId: string, handler: () => T | Promise<T>, options?): Promise<ProcessOnceResult<T>>
```

Runs `handler` once per event id: `{ status: "processed", result }`, `{ status: "duplicate" }` or `{ status: "in_progress" }` (answer 409 so 9in retries). If the handler throws, the id is released and the error rethrown. Options: `ttlSeconds` (default 3 days), `lockSeconds` (default 300), `prefix`.

`EventStore` is `{ add(key, value, ttlSeconds): Promise<boolean> (atomic), get, set, delete }`. `MemoryEventStore` implements it for one process.

## adapterHandler

```ts
adapterHandler(handlers: AdapterHandlers): (request: Request) => Promise<Response>
```

Implements the [adapter protocol](https://9in.io/docs/reference/adapter-protocol/): verifies requests, routes `/authenticate`, `/user` and `/event`, and signs responses.

| Handler | |
|---|---|
| `secret` | string or string[] (rotation) |
| `authenticate(identifier, password)` | Return an `AdapterUser`, `"invalid"` or `"blocked"` |
| `findUser(query)` | `{ id }` or `{ identifier }` → `AdapterUser` or `null` |
| `onEvent(event)` | Optional: `otp.requested`, `user.updated`, `user.password_changed` |
| `toleranceSeconds` | Default 300 |

`signPayload(secret, method, path, body)` and `verifySignature(secret, header, method, path, body, toleranceSeconds?)` expose the signature scheme for other frameworks.

## NineError codes

| Code | Meaning |
|---|---|
| `login_required` | No session: call `login()` |
| `invalid_state` | The callback doesn't match a login in progress (another tab, a replay) |
| `missing_code` | The callback URL has no code |
| `invalid_issuer` | The callback's `iss` isn't your issuer |
| `invalid_id_token` | The ID token failed validation |
| `invalid_token` | An access token failed verification |
| `insufficient_scope` | A required scope is missing |
| `insecure_issuer` | The issuer isn't https (and isn't localhost) |
| `discovery_failed`, `jwks_unavailable`, `userinfo_failed` | 9in couldn't be reached or answered unexpectedly |
| `invalid_signature`, `invalid_payload`, `invalid_event` | A webhook or adapter request failed verification |
| `invalid_options` | A verifier was created without what its mode needs |
| OAuth errors | `access_denied`, `invalid_grant`… from the authorization server, passed through |
