Reference

JavaScript SDK

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

View .md llms.txt

@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
ImportFor
@9in-io/sdkSigning users in from browser apps: NineAuth
@9in-io/sdk/serverBack 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);
OptionType
issuerstringYour tenant's issuer, e.g. https://acme.9in.io
clientIdstring
redirectUristringA registered callback URL
scopestringDefault "openid profile email offline_access"
audiencestringAPI identifier for access tokens
storageStorageLikeWhere tokens are kept (default localStorage, else memory)
transientStorageStorageLikeWhere the PKCE verifier and state wait during the redirect (default sessionStorage)
localestringLanguage of the hosted pages (ui_locales)
navigate(url) => voidDefault window.location.assign
fetchtypeof fetchCustom fetch (tests, React Native)
allowInsecureIssuerboolean

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

MethodReturns
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()booleanA 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
issuerExactly the token's iss
audiencestring or string[]: the API identifier(s) accepted
scopestring (space-separated) or string[]: all required
secretThe API's HMAC secret, for HS256/384/512 tokens
clockToleranceSeconds, 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, clientSecretA confidential application, for introspection
cacheTtlSeconds a hybrid answer is trusted without news (default 300)
cacheSizeMost 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.

MethodAPI 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

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: verifies requests, routes /authenticate, /user and /event, and signs responses.

Handler
secretstring 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
toleranceSecondsDefault 300

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

#NineError codes

CodeMeaning
login_requiredNo session: call login()
invalid_stateThe callback doesn't match a login in progress (another tab, a replay)
missing_codeThe callback URL has no code
invalid_issuerThe callback's iss isn't your issuer
invalid_id_tokenThe ID token failed validation
invalid_tokenAn access token failed verification
insufficient_scopeA required scope is missing
insecure_issuerThe issuer isn't https (and isn't localhost)
discovery_failed, jwks_unavailable, userinfo_failed9in couldn't be reached or answered unexpectedly
invalid_signature, invalid_payload, invalid_eventA webhook or adapter request failed verification
invalid_optionsA verifier was created without what its mode needs
OAuth errorsaccess_denied, invalid_grant… from the authorization server, passed through