Sessions + OAuth (GitHub, Google) mit Drizzle, für Hono und Next.js
Code9 DateienKontext~670 TokensPrüfung bestanden
Installieren
$
genpm add @core/authWas du bekommst
- Quellcode in src/lib/auth/, 9 Dateien. (23,2 kB)
- KI-Regeln in src/lib/auth/AGENTS.md, dazu Regeldateien für die IDE.
- Umgebungsvariablen in .env.example ergänzt: AUTH_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET.
- Löst @core/db für dich auf.
README
Dieses Paket hat keine README.
~670 Tokens→ src/lib/auth/AGENTS.md→ .cursor/rules/genpm-core-auth.mdc
Genau das liest deine KI, wenn sie in src/lib/auth arbeitet. Sonst wird ihrem Kontext nichts hinzugefügt.
@core/auth — rules for AI agents
Purpose
Cookie sessions (random token, only its SHA-256 stored, 30-day sliding expiry, rotation) and OAuth login with GitHub
and Google via arctic. Tables: users, auth_sessions, oauth_accounts. No roles or permissions (out of scope:
build them in your app on top of users.id). No passwords.
Map
index.ts— public API:getUserFromCookieHeader,signOut,startOAuth,finishOAuth, session functions,User.adapters/hono.ts—sessionMiddleware,requireUser,authRoutes().adapters/next.ts—loginRoute,callbackRoute,logoutRoute,getUser(cookies).schema.ts— Drizzle tables. Depends on../db(@core/db).
Integration
- Env:
AUTH_SECRET(≥ 32 random chars, e.g.openssl rand -base64 32),GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET. Optional Google:GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET. Providers without both vars are disabled. - OAuth app callback URL:
<origin>/auth/callback/github(and/auth/callback/google). - Generate and apply migrations (see
src/lib/db/AGENTS.md). - Hono:
Next.js (App Router): createimport { authRoutes, requireUser, sessionMiddleware } from './lib/auth/adapters/hono.js'; app.use(sessionMiddleware); app.route('/auth', authRoutes()); app.get('/api/me', requireUser, (c) => c.json(c.get('user')));app/auth/login/[provider]/route.tswithexport { loginRoute as GET } from '@/lib/auth/adapters/next', the same forcallback/[provider](callbackRoute) andapp/auth/logout/route.ts(logoutRoute as POST). In Server Components:const user = await getUser(await cookies()). Delete the adapter of the framework you don't use (adapters/hono.tsimportshono). - Login link:
<a href="/auth/login/github?returnTo=/dashboard">. Logout:POST /auth/logout. - Verify: open
/auth/login/github, finish the flow, thenGET /api/mereturns the user.
Conventions
- Read the current user only through
sessionMiddleware/getUser; never parse the cookie yourself. - Other modules reference
users.id(text,usr_…) withonDelete: 'cascade'orset null. - Call
rotateSessionafter a privilege change andinvalidateUserSessionswhen an account is compromised. - If you installed
@core/dbwith--dest, fix the../db/index.jsimports here.
Don't
- Don't store or log session tokens, OAuth codes or
AUTH_SECRET. - Don't link accounts by unverified email;
upsertOAuthUseralready enforces it. - Don't accept absolute
returnToURLs (open redirect); usesafeReturnTo. - Don't make GET requests log users out, and don't disable
HttpOnly/SameSiteon the cookie.
# @core/auth — rules for AI agents
## Purpose
Cookie sessions (random token, only its SHA-256 stored, 30-day sliding expiry, rotation) and OAuth login with GitHub
and Google via `arctic`. Tables: `users`, `auth_sessions`, `oauth_accounts`. No roles or permissions (out of scope:
build them in your app on top of `users.id`). No passwords.
## Map
- `index.ts` — public API: `getUserFromCookieHeader`, `signOut`, `startOAuth`, `finishOAuth`, session functions, `User`.
- `adapters/hono.ts` — `sessionMiddleware`, `requireUser`, `authRoutes()`.
- `adapters/next.ts` — `loginRoute`, `callbackRoute`, `logoutRoute`, `getUser(cookies)`.
- `schema.ts` — Drizzle tables. Depends on `../db` (@core/db).
## Integration
1. Env: `AUTH_SECRET` (≥ 32 random chars, e.g. `openssl rand -base64 32`), `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`.
Optional Google: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`. Providers without both vars are disabled.
2. OAuth app callback URL: `<origin>/auth/callback/github` (and `/auth/callback/google`).
3. Generate and apply migrations (see `src/lib/db/AGENTS.md`).
4. Hono:
```ts
import { authRoutes, requireUser, sessionMiddleware } from './lib/auth/adapters/hono.js';
app.use(sessionMiddleware);
app.route('/auth', authRoutes());
app.get('/api/me', requireUser, (c) => c.json(c.get('user')));
```
Next.js (App Router): create `app/auth/login/[provider]/route.ts` with `export { loginRoute as GET } from '@/lib/auth/adapters/next'`,
the same for `callback/[provider]` (`callbackRoute`) and `app/auth/logout/route.ts` (`logoutRoute as POST`).
In Server Components: `const user = await getUser(await cookies())`.
Delete the adapter of the framework you don't use (`adapters/hono.ts` imports `hono`).
5. Login link: `<a href="/auth/login/github?returnTo=/dashboard">`. Logout: `POST /auth/logout`.
6. Verify: open `/auth/login/github`, finish the flow, then `GET /api/me` returns the user.
## Conventions
- Read the current user only through `sessionMiddleware`/`getUser`; never parse the cookie yourself.
- Other modules reference `users.id` (text, `usr_…`) with `onDelete: 'cascade'` or `set null`.
- Call `rotateSession` after a privilege change and `invalidateUserSessions` when an account is compromised.
- If you installed `@core/db` with `--dest`, fix the `../db/index.js` imports here.
## Don't
- Don't store or log session tokens, OAuth codes or `AUTH_SECRET`.
- Don't link accounts by unverified email; `upsertOAuthUser` already enforces it.
- Don't accept absolute `returnTo` URLs (open redirect); use `safeReturnTo`.
- Don't make GET requests log users out, and don't disable `HttpOnly`/`SameSite` on the cookie.
Der genaue Baum, der nach .genpmignore eingebunden wird. Gepinnt an
// Sesiones con token opaco en cookie (https://lucia-auth.com/sessions/basic): 160 bits aleatorios; en BD solo su
// SHA-256. Expiración deslizante: 30 días, renovada cuando queda menos de la mitad.
import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import { eq } from 'drizzle-orm';
import { type Executor, getDb } from '../db/index.js';
import { authSessions, type Session, type User, users } from './schema.js';
const DAY = 86_400_000;
export const SESSION_TTL_MS = 30 * DAY;
export function generateSessionToken(): string {
return encodeBase32LowerCaseNoPadding(crypto.getRandomValues(new Uint8Array(20)));
}
const sessionId = (token: string) => encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
export async function createSession(token: string, userId: string, db: Executor = getDb()): Promise<Session> {
const [session] = await db
.insert(authSessions)
.values({ id: sessionId(token), userId, expiresAt: new Date(Date.now() + SESSION_TTL_MS) })
.returning();
return session!;
}
export type SessionValidation = { session: Session; user: User } | { session: null; user: null };
/** Valida el token de la cookie. Borra la sesión si caducó y extiende la expiración si toca. */
export async function validateSessionToken(token: string, db: Executor = getDb()): Promise<SessionValidation> {
const id = sessionId(token);
const [row] = await db
.select({ session: authSessions, user: users })
.from(authSessions)
.innerJoin(users, eq(authSessions.userId, users.id))
.where(eq(authSessions.id, id));
if (!row) return { session: null, user: null };
const { session, user } = row;
const now = Date.now();
if (now >= session.expiresAt.getTime()) {
await db.delete(authSessions).where(eq(authSessions.id, id));
return { session: null, user: null };
}
if (now >= session.expiresAt.getTime() - SESSION_TTL_MS / 2) {
session.expiresAt = new Date(now + SESSION_TTL_MS);
await db.update(authSessions).set({ expiresAt: session.expiresAt }).where(eq(authSessions.id, id));
}
return { session, user };
}
export async function invalidateSession(token: string, db: Executor = getDb()): Promise<void> {
await db.delete(authSessions).where(eq(authSessions.id, sessionId(token)));
}
/** Cierra todas las sesiones de un usuario (cambio de credenciales, "cerrar sesión en todos los dispositivos"). */
export async function invalidateUserSessions(userId: string, db: Executor = getDb()): Promise<void> {
await db.delete(authSessions).where(eq(authSessions.userId, userId));
}
/** Rotación: nuevo token para el mismo usuario y el viejo deja de valer (tras login o cambio de privilegios). */
export async function rotateSession(oldToken: string, db: Executor = getDb()): Promise<{ token: string } | null> {
const { user } = await validateSessionToken(oldToken, db);
if (!user) return null;
const token = generateSessionToken();
await createSession(token, user.id, db);
await invalidateSession(oldToken, db);
return { token };
}
Dieses Paket deklariert keine MCP-Server.
| Version | Commit | Veröffentlicht | Prüfung |
|---|---|---|---|
| 1.0.0 | 12c4918 | vor 4 Stunden | ✔ Prüfung bestanden |
- genpm
- @core/db ^1.0.0
- vorgeschlagen
- GenPM schlägt den npm-Befehl vor und führt ihn nur aus, wenn du zustimmst.
- Prüfung
- Prüfung bestanden · 0 Befunde
- Commit
- v1.0.0 → 12c4918f68aa7cb6062a976347bf6a7d691cc218 · nach dem Abruf verifiziert
- Skripte
- Keine. GenPM führt niemals Paketcode aus.
- Lizenz
- MIT
- Meldung
- Stimmt etwas nicht?