Sub-skills
Auth in depth
For your agentCopy this and give it to the AI you're building with.
Run `cubek skill`, then `cubek skill auth`, read all of both and follow them exactly (no CLI: download https://cubek.dev/SKILL.md and https://cubek.dev/skills/auth.md verbatim with curl; don't summarize them). Run `cubek check` after every edit until it is clean.
Sign-up, sign-in, sessions, sign-out, guarded routes, roles, API tokens and rate limits. Accounts are app code: copy this, do not invent a scheme.
Schema
// schema.ts
import { table, t } from "cubek"
export const users = table("users", {
id: t.id(),
email: t.string().unique({ nocase: true }),
passwordHash: t.string(),
role: t.enum(["member", "admin"]).default("member"),
createdAt: t.timestamp().default("now"),
}).index("createdAt")
export const sessions = table("sessions", {
id: t.id(),
tokenHash: t.string().unique(),
userId: t.ref(users),
expiresAt: t.timestamp(),
}).index("userId")
export const apiTokens = table("apiTokens", { id: t.id(), tokenHash: t.string().unique(), userId: t.ref(users), name: t.string() })Sessions
The cookie holds a random token; the database holds only its SHA-256, so a leaked database does not leak sessions.
// src/lib/auth.ts
import { crypto, db, encoding, type Context, type Row } from "cubek"
import type { users } from "../../schema"
export type User = Row<typeof users>
const DAYS = 30
export async function currentUser(c: Context): Promise<User | null> {
const token = c.cookie("sid")
if (token === null) return null
const s = await db.sessions.findOne({ where: { tokenHash: crypto.hash("sha256", token) } })
if (s === null || s.expiresAt < new Date().toISOString()) return null
return await db.users.get(s.userId)
}
export async function startSession(c: Context, userId: string): Promise<void> {
const token = encoding.hex.encode(crypto.randomBytes(32))
const expiresAt = new Date(Date.now() + DAYS * 86400000).toISOString()
await db.sessions.insert({ tokenHash: crypto.hash("sha256", token), userId, expiresAt })
c.setCookie("sid", token, { httpOnly: true, sameSite: "lax", path: "/", maxAge: DAYS * 86400 })
}
export async function endSession(c: Context): Promise<void> {
const token = c.cookie("sid")
if (token !== null) await db.sessions.deleteWhere({ tokenHash: crypto.hash("sha256", token) })
c.setCookie("sid", "", { maxAge: 0, path: "/" })
}
// API clients: `Authorization: Bearer <token>`, created once and shown once.
export async function tokenUser(c: Context): Promise<string | null> {
const h = c.header("authorization")
if (h === null || !h.startsWith("Bearer ")) return null
const t = await db.apiTokens.findOne({ where: { tokenHash: crypto.hash("sha256", h.slice(7)) } })
return t === null ? null : t.userId
}Routes
// src/app.tsx
import { app, crypto, db, rateLimit, validate } from "cubek"
import { currentUser, endSession, startSession } from "./lib/auth"
const credentials = validate.object({
email: validate.string({ email: true, max: 254 }),
password: validate.string({ min: 8, max: 200 }),
})
// Brute force: per IP on the auth forms.
app.use("/auth/*", rateLimit({ limit: 10, window: "1m" }))
app.post("/auth/signup", async (c) => {
const p = credentials.safeParse(await c.body())
if (!p.success) return c.text(p.error, 400)
if ((await db.users.findOne({ where: { email: p.data.email } })) !== null) return c.text("that email has an account", 409)
const user = await db.users.insert({ email: p.data.email, passwordHash: crypto.password.hash(p.data.password) })
await startSession(c, user.id)
return c.redirect("/", 303)
})
app.post("/auth/signin", async (c) => {
const p = credentials.safeParse(await c.body())
if (!p.success) return c.text("wrong email or password", 401)
const user = await db.users.findOne({ where: { email: p.data.email } })
if (user === null || !crypto.password.verify(p.data.password, user.passwordHash)) return c.text("wrong email or password", 401)
await startSession(c, user.id)
return c.redirect("/", 303)
})
app.post("/auth/signout", async (c) => {
await endSession(c)
return c.redirect("/", 303)
})
// Guard: a middleware for a prefix (it runs for every path under it).
app.use("/admin/*", async (c, next) => {
const u = await currentUser(c)
if (u === null) return c.redirect("/signin", 303)
if (u.role !== "admin") return c.text("forbidden", 403)
await next()
})
app.get("/admin/users", async (c) => c.json(await db.users.find({ orderBy: { createdAt: "desc" }, limit: 100 })))
// Per-user limit: `key` returns the user id (null falls back to the IP).
const perUser = rateLimit({ limit: 30, window: "1h", key: async (c) => { const u = await currentUser(c); return u === null ? null : u.id } })
app.post("/api/generate", perUser, async (c) => {
const u = await currentUser(c)
return u === null ? c.json({ error: "sign in" }, 401) : c.json({ ok: true })
})- A guard on
/admin/*also covers/admin/login: keep sign-in pages outside the guarded prefix, or guard route by route. - Handlers call
currentUser(c)again (noc.set); it is one indexed read. - Every live query and RPC checks the user itself: anyone can call them.
- Emails are unique ignoring case (
nocase); still compare the same way. - Password reset: a random token (store its hash, 1 h expiry), mailed as a
link with
c.origin(); the link's POST sets the new hash and deletes the user's sessions (deleteWhere({ userId })). - CSRF:
sameSite: "lax"cookies plus POST for every write cover forms; never change state in a GET. - Exact quotas (per day, per plan) instead of windows:
kv.incr(key, 1, { ttl: "1d" })and compare.