JavaScript SDK
Every class, function and option of @9in-io/sdk and @9in-io/sdk/server.
@9in-io/sdk has no dependencies and runs in browsers, Node 18+, Cloudflare Workers, Deno and Bun. It ships TypeScript types.
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)
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)
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
const verifier = createTokenVerifier(options: TokenVerifierOptions): TokenVerifierVerifyOptions (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
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 directly.
#verifyWebhook
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
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
adapterHandler(handlers: AdapterHandlers): (request: Request) => Promise<Response>Implements the 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 |