newX 1.3 — islands to disk, server-mode islands, image proxy

Packages

@thexjs/auth

Plug-and-play authentication for X apps. Add credentials (username/password) and OAuth2 — including a preconfigured GitHub provider — with one defineAuth() call, a sessions table in SQLite or Postgres via the framework's data layer, and a single catch-all API route.

terminal
bun add @thexjs/auth

Quick start

Define your providers and session store once:

lib/auth.ts
import { defineAuth, createSQLiteSessionStore, hashPassword, verifyPassword } from "@thexjs/auth";export const auth = defineAuth({  secret: process.env.AUTH_SECRET!,  store: createSQLiteSessionStore(), // or createPostgresSessionStore(client)  providers: [    {      id: "local",      name: "Local",      type: "credentials",      async authorize({ email, password }) {        const user = await db.query("SELECT * FROM users WHERE email = ?").get(email);        if (!user) return null;        if (!(await verifyPassword(password, user.password_hash))) return null;        return { id: String(user.id), name: user.name, email: user.email };      },    },    {      id: "github",      name: "GitHub",      type: "oauth",      clientId: process.env.GITHUB_CLIENT_ID!,      clientSecret: process.env.GITHUB_CLIENT_SECRET!,    },  ],});

The github provider is a preset: give it a client ID and secret and the authorization, token, and user-info URLs are wired up for you. A generic OAuth2 provider is available too for any other authorization-code provider.

Wire it up with one catch-all API route:

api/auth/[...auth].ts
import { auth } from "../../lib/auth";export async function POST(req: Request) {  return auth.handleRequest(req);}export async function GET(req: Request) {  return auth.handleRequest(req);}

Create users at sign-up with hashPassword (Argon2id) and store the hash — never plaintext:

signup
import { hashPassword } from "@thexjs/auth";await hashPassword("correct horse battery staple");

Endpoints

handleRequest routes the path below api/auth:

routes
Route                        Method  Purpose/api/auth/signin/<id>        POST    credentials provider: form or multipart body with the                                     provider's fields (e.g. email, password)/api/auth/signin/<id>        GET     OAuth2 provider: redirects the browser to the provider's                                     authorization URL/api/auth/callback/<id>      GET     OAuth2 callback: exchanges the code, validates the state                                     challenge, signs the user in/api/auth/signout            POST    revokes the session and clears the cookie/api/auth/session            GET     JSON { "user": { ... } } or 401

A sign-in form POSTs to /api/auth/signin/localand, after success, the browser follows the 302 to successRedirect (default /). For OAuth, the button or link is just a GET to /api/auth/signin/github.

Reading the session

middleware or loader
const session = await auth.getSession(request);if (!session) return new Response("Unauthorized", { status: 401 });session.user; // { id, name?, email? } snapshot from sign-in

getSession hashes the x_session cookie, looks up the token in the store, and returns null for expired or revoked sessions. setSessionCookie(res, user, provider) and clearSessionCookie(res, req?) are also exported for programmatic flows.

Security

  • Passwords — Argon2id via Bun.password ( hashPassword / verifyPassword).
  • Session tokens — opaque random strings; only an HMAC-SHA256 digest (keyed by secret) is stored, so a database leak doesn't expose usable session cookies. Tokens are random 128-bit values, revocable, and expire after sessionMaxAge (default 7 days).
  • OAuth state — an x_oauth_state cookie challenge must match the state param on the callback (HMAC'd, 5-minute expiry), preventing login-CSRF and session-fixation via crafted callbacks.
  • CSRF — POST endpoints ( signin, signout ) run the core checkCsrf automatically — Origin/Referer verification by default, or requireToken for double-submit defense in depth — and reject non-conforming requests with 403. See Security for how the module is configured.
  • CookiesHttpOnly, SameSite=Lax, Secure in production.

Set a stable secret in production. If omitted, a random per-process secret is generated and a warning is printed, which means sessions won't survive restarts.

Session stores

Both stores use a single x_sessions table and implement the SessionStore interface ( create, find, revoke), so you can bring your own:

stores
createSQLiteSessionStore({ path: "data/auth.db" });   // default: data/auth.dbcreatePostgresSessionStore(connectPostgres({ url: process.env.DATABASE_URL }));

The Postgres store ensures the table lazily on first use and takes a client returned by connectPostgres from @thexjs/core/data, so it inherits the connection pool, TLS policy, and retry behavior of the framework. See Data Layer for the underlying stores.