@gauts/auth 0.3.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,300 +1,121 @@
1
1
  # `@gauts/auth`
2
2
 
3
- Reusable password authentication and database-backed opaque sessions for Node.js applications.
3
+ Database-backed password authentication and opaque browser sessions for Node.js applications.
4
+
5
+ `@gauts/auth` provides the reusable authentication layer: password hashing, session lifecycle, secure cookies, database validation, optional short caching, and framework adapters. The application keeps control of registration, account lookup, authorization, routes, responses, and UI.
6
+
7
+ ## Features
8
+
9
+ | Capability | Support | Default |
10
+ | ---------------------------------- | :-----: | ------------------- |
11
+ | Argon2id password hashing | ✅ | Enabled |
12
+ | bcrypt password hashing | ✅ | Opt-in |
13
+ | Opaque server-side sessions | ✅ | Enabled |
14
+ | Database-backed validation | ✅ | Enabled |
15
+ | Sliding session renewal | ✅ | Every 24 hours |
16
+ | Absolute session lifetime | ✅ | 30 days |
17
+ | Signed browser cache | ✅ | Disabled |
18
+ | Full User-Agent validation | ✅ | Enabled |
19
+ | IP validation | ✅ | Disabled |
20
+ | Platform validation | ✅ | Disabled |
21
+ | Hono adapter | ✅ | Available |
22
+ | Prisma adapter | ✅ | Available |
23
+ | Next.js renewal adapter | ✅ | Available |
24
+ | Session listing and revocation | ✅ | Available |
25
+ | Token rotation | ❌ | Stable opaque token |
26
+ | JWT sessions | ❌ | Not used |
27
+ | Redis requirement | ❌ | Not required |
28
+ | Registration, OAuth, OTP, or email | ❌ | Application-owned |
29
+ | Route roles and permissions | ❌ | Application-owned |
30
+
31
+ “Session renewal” extends the existing session expiry when activity continues. It is not a refresh-token flow and does not rotate the opaque browser token.
4
32
 
5
- The package stores only a SHA-256 token hash in the database. Browser integration uses three cookies with separate responsibilities:
6
-
7
- ```text
8
- session cookie -> opaque token
9
- cache cookie -> short signed session snapshot
10
- renew cookie -> untrusted renewAt timestamp
11
- ```
12
-
13
- Only the opaque session token authenticates. It remains stable during sliding renewal, but the session can never exceed its configured maximum lifetime. The optional cache is disabled when omitted and never replaces database validation for unsafe methods, renewal, logout, WebSockets, or direct core calls.
33
+ ## Installation
14
34
 
15
- ## Requirements
35
+ ### Requirements
16
36
 
17
37
  - Node.js 22 or newer.
18
- - A database adapter, or a Prisma client containing the required schema contract.
19
- - Hono 4 when using `@gauts/auth/hono`.
20
- - Next.js 15 or newer when using `@gauts/auth/next`.
21
-
22
- ## Installation
38
+ - A database adapter.
39
+ - Hono 4 when using the Hono adapter.
40
+ - Next.js 15 or newer when using the Next.js adapter.
23
41
 
24
- For Hono and Prisma:
42
+ ### Hono and Prisma
25
43
 
26
44
  ```bash
27
45
  npm install @gauts/auth hono @prisma/client
28
46
  ```
29
47
 
30
- For the Next.js adapter, the application must already use Next.js:
48
+ ### Next.js
31
49
 
32
50
  ```bash
33
51
  npm install @gauts/auth next
34
52
  ```
35
53
 
36
- `hono` and `next` are optional peer dependencies. The Prisma adapter receives the consuming application's generated client and does not import Prisma at runtime.
37
-
38
- ## Package entry points
39
-
40
- | Import | Purpose |
41
- | --- | --- |
42
- | `@gauts/auth` | Framework-independent password and session core types. |
43
- | `@gauts/auth/prisma` | Prisma implementation of the database adapter. |
44
- | `@gauts/auth/hono` | Hono cookies, session methods, and middleware. |
45
- | `@gauts/auth/next` | Next.js renewal scheduling and `Set-Cookie` forwarding. |
46
-
47
- ## Flow
48
-
49
- Login:
50
-
51
- ```text
52
- credentials accepted
53
- -> generate 256-bit opaque token
54
- -> store SHA-256 token hash in DB
55
- -> load current account and user relations
56
- -> validate configured status and role rules
57
- -> write opaque session token cookie
58
- -> write renewAt as Unix seconds
59
- -> optionally write signed short cache
60
- ```
61
-
62
- Protected `GET` or `HEAD`:
63
-
64
- ```text
65
- opaque session token
66
- -> valid signed cache bound to token and client?
67
- -> yes: expose cached session, account, and user
68
- -> no: perform indexed DB validation and issue a fresh cache
69
- ```
70
-
71
- Unsafe methods and direct core calls:
72
-
73
- ```text
74
- opaque session token
75
- -> SHA-256 token
76
- -> indexed DB lookup
77
- -> expiry, revocation, account, user, and client validation
78
- -> clear any existing cache after successful unsafe validation
79
- -> route
80
- ```
81
-
82
- Renewal:
83
-
84
- ```text
85
- Next reads renewAt from the renewal cookie
86
- -> valid future Unix timestamp: no API call
87
- -> missing, invalid, or due timestamp: POST /auth/renew
88
- -> API performs full DB validation
89
- -> DB expires_at = min(now + ttl, created_at + maxLifetime)
90
- -> Set-Cookie with same token, a new renewAt, and a fresh cache
91
- ```
92
-
93
- `auth.session.resolve()` is always DB-backed and read-only. Only the explicit renewal operation updates database expiry, and neither activity nor renewal can extend the session beyond `maxLifetime`.
94
-
95
- ## Prisma schema contract
96
-
97
- The Prisma adapter resolves authentication through this relation chain:
98
-
99
- ```text
100
- account_sessions -> account -> user
101
- ```
102
-
103
- The default Prisma client delegate is `account_sessions`. A different session model delegate can be selected through `config.table`.
104
-
105
- ### Complete minimal schema
106
-
107
- This is a complete MySQL/MariaDB example. If `users` and `user_accounts` already exist, merge the required fields and relations into those models instead of duplicating them.
108
-
109
- ```prisma
110
- model users {
111
- id String @id @default(uuid()) @db.VarChar(255)
112
- role String @db.VarChar(255)
113
- status String @db.VarChar(255)
114
-
115
- accounts user_accounts[]
116
- }
117
-
118
- model user_accounts {
119
- id String @id @default(uuid()) @db.VarChar(255)
120
- user_id String @db.VarChar(255)
121
- email String @db.VarChar(255)
122
- name String @db.VarChar(255)
123
- role String @db.VarChar(255)
124
- status String @db.VarChar(255)
125
- timezone String? @db.VarChar(255)
126
-
127
- user users @relation(fields: [user_id], references: [id], onDelete: Cascade)
128
- sessions account_sessions[]
129
-
130
- @@index([user_id])
131
- }
132
-
133
- model account_sessions {
134
- id String @id @default(uuid()) @db.VarChar(255)
135
- account_id String @db.VarChar(255)
136
- token_hash String @unique @db.VarChar(64)
137
- ip String? @db.VarChar(45)
138
- platform String? @db.VarChar(255)
139
- agent String? @db.Text
140
- expires_at DateTime @db.Timestamp(0)
141
- revoked_at DateTime? @db.Timestamp(0)
142
- created_at DateTime @default(now()) @db.Timestamp(0)
143
- updated_at DateTime? @db.Timestamp(0)
144
-
145
- account user_accounts @relation(fields: [account_id], references: [id], onDelete: Cascade)
146
-
147
- @@index([account_id])
148
- @@index([expires_at])
149
- @@index([revoked_at])
150
- }
151
- ```
152
-
153
- The adapter requires these Prisma field and relation names:
154
-
155
- | Path | Required fields |
156
- | --- | --- |
157
- | Session model | `id`, `account_id`, `token_hash`, `ip`, `platform`, `agent`, `expires_at`, `revoked_at`, `created_at`, `updated_at` |
158
- | `account` relation | `id`, `email`, `name`, `role`, `status`, `timezone` |
159
- | `account.user` relation | `id`, `role`, `status` |
160
-
161
- The relation fields must be named `account` and `user`, because those are the names selected by the adapter. Prisma also requires the inverse relations; their field names (`sessions` and `accounts` above) can be changed because the adapter never queries them.
54
+ `hono` and `next` are optional peer dependencies. The Prisma adapter receives the application's generated Prisma client and does not import Prisma at runtime.
162
55
 
163
- Application models may contain additional fields, defaults, indexes, and relations. `role` and `status` may use application-specific Prisma enums instead of `String`; Prisma returns both as strings to the adapter. For other database providers, replace the native `@db.*` annotations with compatible types and keep `agent` large enough to store the complete User-Agent.
56
+ ### Package entry points
164
57
 
165
- `config.table` is the Prisma client delegate name, not the physical database table name. For example, a Prisma model named `AdminSession` mapped with `@@map("account_sessions")` normally uses the `adminSession` delegate.
58
+ | Import | Purpose |
59
+ | -------------------- | --------------------------------------------------------- |
60
+ | `@gauts/auth` | Password service, session core, errors, and public types. |
61
+ | `@gauts/auth/prisma` | Prisma database adapter. |
62
+ | `@gauts/auth/hono` | Hono cookies, methods, and middleware. |
63
+ | `@gauts/auth/next` | Next.js renewal scheduling and `Set-Cookie` forwarding. |
166
64
 
167
- Create migrations through the consuming application's normal Prisma workflow. The package never creates or runs migrations.
65
+ ## Quick start
168
66
 
169
- ## Hono quick start
67
+ The Hono adapter does not create routes automatically. The application defines its own login, renewal, logout, and protected endpoints.
170
68
 
171
- The Hono adapter provides methods and middleware; it does not register routes automatically. The application remains responsible for creating its login, renewal, logout, and protected endpoints.
172
-
173
- ### Create one auth instance
69
+ ### 1. Create the auth instance
174
70
 
175
71
  ```ts
176
72
  import { createHonoAuth } from "@gauts/auth/hono";
177
73
  import { createPrismaAdapter } from "@gauts/auth/prisma";
178
74
 
179
- export const auth = createHonoAuth({
180
- secret: process.env.AUTH_SECRET,
75
+ import { prisma } from "./db.js";
181
76
 
77
+ export const auth = createHonoAuth({
182
78
  db: createPrismaAdapter({
183
79
  client: prisma,
184
80
  config: {
185
- account: {
186
- status: ["ACTIVE", "PENDING"],
187
- },
188
- user: {
189
- status: ["ACTIVE", "PENDING"],
81
+ access: {
82
+ account: {
83
+ allowedStatuses: ["ACTIVE"],
84
+ },
85
+ user: {
86
+ allowedStatuses: ["ACTIVE"],
87
+ },
190
88
  },
191
89
  },
192
90
  }),
193
-
194
- getIp: (c) => getTrustedClientIp(c),
195
-
196
- session: {
197
- validation: ["agent"],
198
- },
199
-
200
- cache: {
201
- ttl: 60,
202
- },
203
91
  });
204
92
  ```
205
93
 
206
- `secret` is required only when `cache` is configured. Supply at least 32 high-entropy bytes from the API environment. It is never shared with the Next.js application.
207
-
208
- `createHonoAuth` is the normal Hono entry point: it creates the framework-independent core and attaches the Hono methods in one object. Use `createAuth` plus `createHonoAdapter` only when the same core instance must be composed manually:
94
+ This uses the defaults:
209
95
 
210
- The resolved cookie names are exposed on the auth instance, including defaults:
211
-
212
- ```ts
213
- auth.cookie.name; // "__sec"
214
- auth.cookie.cacheName; // "__cac"
215
- auth.cookie.renewName; // "__ren"
216
- ```
217
-
218
- ```ts
219
- import { createAuth } from "@gauts/auth";
220
- import { createHonoAdapter } from "@gauts/auth/hono";
221
-
222
- const core = createAuth({ db });
223
- const hono = createHonoAdapter({
224
- auth: core,
225
- cache: { ttl: 60 },
226
- getIp: (c) => getTrustedClientIp(c),
227
- secret: process.env.AUTH_SECRET,
228
- });
96
+ ```text
97
+ password Argon2id
98
+ session TTL 7 days
99
+ renewal every 24 hours
100
+ max lifetime 30 days
101
+ validation User-Agent
102
+ cache disabled
103
+ cookies __ses, __cac, __ren
229
104
  ```
230
105
 
231
- Do not create a second core for the adapter; pass the existing `core` instance through `auth`.
232
-
233
- Only `account_id` is stored in the session row. The Prisma adapter loads the current `account` and `user` relations during authentication; no dynamic account data is copied into the session.
234
-
235
- ### Type the application
106
+ ### 2. Add the routes
236
107
 
237
108
  ```ts
238
109
  import { Hono } from "hono";
239
110
  import type { HonoAuthEnv } from "@gauts/auth/hono";
240
111
 
241
- const app = new Hono<HonoAuthEnv>();
242
- ```
243
-
244
- `auth.requireSession` installs:
245
-
246
- ```ts
247
- const session = c.get("session");
248
- const account = c.get("account");
249
- const user = c.get("user");
250
- ```
251
-
252
- The values have these shapes:
112
+ import { auth } from "./auth.js";
113
+ import { DUMMY_PASSWORD_HASH } from "./password.js";
253
114
 
254
- ```ts
255
- type AuthAccount = {
256
- email: string;
257
- id: string;
258
- name: string;
259
- role: string;
260
- status: string;
261
- timezone: string | null;
262
- user: AuthUser;
263
- };
264
-
265
- type AuthUser = {
266
- id: string;
267
- role: string;
268
- status: string;
269
- };
270
-
271
- type Session = {
272
- account_id: string;
273
- client: {
274
- agent: string | null;
275
- ip: string | null;
276
- platform: string | null;
277
- };
278
- created_at: Date;
279
- expires_at: Date;
280
- id: string;
281
- renew_at: Date;
282
- };
283
- ```
284
-
285
- ### Login
286
-
287
- The application owns input validation, credential lookup, rate limiting, and error responses. The adapter access rules validate current account and user status/role before the session is accepted.
288
-
289
- ```ts
290
- const DUMMY_PASSWORD_HASH =
291
- "$argon2id$v=19$m=65536,p=4,t=3$PUotpfVXonc0VRFuV1pKZQ$oxxA8DMvGRTSbZvh2Dkokeyih9sbKeodWYROqVxP9BI";
115
+ const app = new Hono<HonoAuthEnv>();
292
116
 
293
117
  app.post("/auth/login", async (c) => {
294
- const body = await c.req.json<{
295
- email: string;
296
- password: string;
297
- }>();
118
+ const body = await c.req.json<{ email: string; password: string }>();
298
119
  const account = await findAccount(body.email);
299
120
  const passwordValid = await auth.password.verify({
300
121
  password: body.password,
@@ -312,328 +133,514 @@ app.post("/auth/login", async (c) => {
312
133
 
313
134
  return c.json({ authenticated: true });
314
135
  });
315
- ```
316
136
 
317
- The dummy hash ensures that unknown accounts still perform the configured password verification. Precompute it once with the same algorithm and cost as the application; never generate it inside the request handler. Keep the response identical for unknown accounts and invalid passwords.
318
-
319
- Only `account_id` is persisted by the session. Email, roles, statuses, password hashes, and other dynamic account data never enter the session table.
137
+ app.post("/auth/renew", async (c) => {
138
+ await auth.renewSession(c);
139
+ return c.body(null, 204);
140
+ });
320
141
 
321
- ### Protect routes
142
+ app.post("/auth/logout", async (c) => {
143
+ await auth.revokeSession(c);
144
+ return c.body(null, 204);
145
+ });
322
146
 
323
- ```ts
324
147
  app.get("/account", auth.requireSession, (c) => {
325
148
  return c.json({
326
149
  account: c.get("account"),
327
- session_id: c.get("session").id,
150
+ session: c.get("session"),
328
151
  user: c.get("user"),
329
152
  });
330
153
  });
331
154
  ```
332
155
 
333
- `requireSession` authenticates the request. With cache configured, only `GET` and `HEAD` may use it. Every other method validates through the database and clears the short cache after successful validation. It does not apply application-specific route roles or permissions.
156
+ Precompute `DUMMY_PASSWORD_HASH` once with the same algorithm and cost as the application. This ensures unknown accounts perform equivalent password verification work. Keep the response identical for unknown accounts and incorrect passwords.
334
157
 
335
- ### Renewal endpoint
158
+ ### 3. Enable the optional cache
336
159
 
337
160
  ```ts
338
- app.post("/auth/renew", async (c) => {
339
- await auth.renewSession(c);
340
- return c.body(null, 204);
161
+ export const auth = createHonoAuth({
162
+ cache: {
163
+ ttl: 60,
164
+ },
165
+ db,
166
+ secret: requiredEnv("AUTH_SECRET"),
341
167
  });
342
168
  ```
343
169
 
344
- `renewSession` always performs full database validation. It independently derives whether renewal is due from `created_at` and the last database renewal; the renewal marker never authorizes renewal.
170
+ `AUTH_SECRET` must contain at least 32 high-entropy bytes. It stays in the API and is never shared with Next.js.
171
+
172
+ `requiredEnv()` represents an application helper that returns a non-empty environment string or fails during startup.
345
173
 
346
- If renewal is not yet due, database expiry is not changed. The API still returns the authoritative session cookie, renewal marker, and cache.
174
+ ## Configuration reference
347
175
 
348
- ### Logout
176
+ ### `createHonoAuth()`
177
+
178
+ | Property | Type / allowed values | Required | Default | Description |
179
+ | ---------- | --------------------- | :---------------------: | ----------------- | --------------------------------------------------------------------------------------------------- |
180
+ | `db` | `DbAdapter` | ✅ | — | Authoritative session persistence and account loading. |
181
+ | `getIp` | `HonoGetIp` | Only with IP validation | Omitted | Returns the client IP from a source trusted by the application. May be synchronous or asynchronous. |
182
+ | `password` | `PasswordConfig` | ❌ | Argon2id defaults | Password hashing and verification configuration. |
183
+ | `session` | `SessionConfig` | ❌ | Session defaults | Expiry, renewal, and client validation configuration. |
184
+ | `cookie` | `HonoCookieConfig` | ❌ | Cookie defaults | Names, domain, path, SameSite, and Secure settings. |
185
+ | `cache` | `{ ttl: number }` | ❌ | Disabled | Enables the short signed browser cache. |
186
+ | `secret` | `string` | When `cache` is enabled | — | HMAC secret for the signed cache. Minimum 32 UTF-8 bytes. |
349
187
 
350
188
  ```ts
351
- app.post("/auth/logout", async (c) => {
352
- await auth.revokeSession(c);
353
- return c.body(null, 204);
354
- });
189
+ type HonoGetIp = (c: Context) => Promise<string | null | undefined> | string | null | undefined;
355
190
  ```
356
191
 
357
- Logout marks the database session as revoked and then clears all three cookies. If database revocation fails, the cookies are not cleared and the error is propagated.
192
+ When `getIp` is omitted, the adapter stores `ip: null` and does not read IP headers automatically. Configuring `session.validation` with `"ip"` requires `getIp` and fails during initialization when it is missing.
358
193
 
359
- ## Next.js renewal adapter
194
+ ### Password
360
195
 
361
- The Next.js adapter does not authenticate sessions. It calls the private API renewal URL when `renewAt` is missing, invalid, or due.
196
+ #### Argon2id
362
197
 
363
- ```ts
364
- import { createNextAuth } from "@gauts/auth/next";
198
+ Argon2id is selected when `password.algorithm` is omitted or set to `"argon2id"`.
365
199
 
366
- export const nextAuth = createNextAuth({
367
- renewUrl: `${process.env.NEXT_PRIVATE_API_URL}/auth/renew`,
368
- });
200
+ | Property | Type / allowed values | Default | Description |
201
+ | ------------- | --------------------------- | -----------: | ----------------------------------------------------------------- |
202
+ | `algorithm` | `"argon2id"` | `"argon2id"` | Password algorithm used for both hashing and verification. |
203
+ | `hashLength` | Integer `16`–`64` | `32` | Output hash length in bytes. |
204
+ | `maxBytes` | Integer `1`–`1,048,576` | `1024` | Maximum UTF-8 password size accepted by hashing and verification. |
205
+ | `memoryCost` | Integer `8,192`–`1,048,576` | `65,536` | Argon2 memory cost in KiB. |
206
+ | `parallelism` | Integer `1`–`16` | `4` | Number of parallel lanes. |
207
+ | `timeCost` | Integer `1`–`10` | `3` | Number of Argon2 iterations. |
208
+
209
+ ```ts
210
+ password: {
211
+ algorithm: "argon2id",
212
+ }
369
213
  ```
370
214
 
371
- The controlled header list is also exported for application fetchers that need to follow the same forwarding policy:
215
+ #### bcrypt
216
+
217
+ Applications with existing bcrypt hashes must select bcrypt explicitly.
218
+
219
+ | Property | Type / allowed values | Default | Description |
220
+ | ---------------- | -------------------------------------- | -------: | ------------------------------------------------------- |
221
+ | `algorithm` | `"bcrypt"` | Required | Selects bcrypt for both hashing and verification. |
222
+ | `maxBytes` | Integer `1`–`72` | `72` | Maximum UTF-8 size accepted for new passwords. |
223
+ | `rounds` | Integer `4`–`31` | `12` | bcrypt cost factor. |
224
+ | `verifyMaxBytes` | Integer from `maxBytes` to `1,048,576` | `72` | Maximum input accepted while verifying existing hashes. |
372
225
 
373
226
  ```ts
374
- import { FORWARD_HEADERS } from "@gauts/auth/next";
227
+ password: {
228
+ algorithm: "bcrypt",
229
+ }
375
230
  ```
376
231
 
377
- Use it in `proxy.ts` before returning the browser-facing response:
232
+ The package does not detect algorithms, migrate hashes, rehash passwords, or fall back to another algorithm.
233
+
234
+ ### Session
235
+
236
+ All time values are seconds.
237
+
238
+ | Property | Type / allowed values | Default | Description |
239
+ | --------------- | ----------------------------------------------- | --------------------: | ------------------------------------------------------------------------- |
240
+ | `maxLifetime` | Integer from `ttl` to `31,536,000` | `2,592,000` (30 days) | Maximum session lifetime from the original login, regardless of activity. |
241
+ | `renewInterval` | Integer `1` to `ttl - 1` | `86,400` (24 hours) | Minimum interval before sliding renewal is due. |
242
+ | `ttl` | Integer `60`–`31,536,000` | `604,800` (7 days) | Inactivity lifetime assigned at login and renewal. |
243
+ | `validation` | Unique array of `"agent"`, `"ip"`, `"platform"` | `["agent"]` | Client fields that must match the stored session exactly. |
378
244
 
379
245
  ```ts
380
- import type { NextRequest } from "next/server";
381
- import { NextResponse } from "next/server";
246
+ session: {
247
+ maxLifetime: 60 * 60 * 24 * 30,
248
+ renewInterval: 60 * 60 * 24,
249
+ ttl: 60 * 60 * 24 * 7,
250
+ validation: ["agent", "ip"],
251
+ }
252
+ ```
382
253
 
383
- export const proxy = async (request: NextRequest) => {
384
- const response = NextResponse.next();
385
- const renewal = await nextAuth.renew({
386
- request,
387
- response,
388
- });
254
+ Validation values:
389
255
 
390
- if (renewal.status === 401) {
391
- return NextResponse.redirect(new URL("/login", request.url));
392
- }
256
+ | Enum | Source | Behavior |
257
+ | ------------ | ------------------------------------- | -------------------------------------------------------- |
258
+ | `"agent"` | Complete `User-Agent` header | Enabled by default. The complete value must match. |
259
+ | `"ip"` | Application-provided `getIp()` result | IPv4 and IPv6 are canonicalized before exact comparison. |
260
+ | `"platform"` | `Sec-CH-UA-Platform` header | Normalized and compared exactly. |
393
261
 
394
- if (renewal.status !== null && renewal.status >= 500) {
395
- return NextResponse.redirect(new URL("/maintenance", request.url));
396
- }
262
+ Every selected field is required during session creation and validation. A mismatch revokes the database session.
397
263
 
398
- return renewal.response;
399
- };
264
+ Expiry is derived as follows:
265
+
266
+ ```text
267
+ maxExpiresAt = created_at + maxLifetime
268
+ expires_at = min(now + ttl, maxExpiresAt)
269
+ renew_at = min((updated_at ?? created_at) + renewInterval, maxExpiresAt)
400
270
  ```
401
271
 
402
- Result semantics:
272
+ ### Cookies
403
273
 
404
- | `attempted` | `status` | Meaning |
405
- | --- | ---: | --- |
406
- | `false` | `null` | Session token exists and `renewAt` is a valid future timestamp. |
407
- | `false` | `401` | Session token is missing or malformed. No API call occurred. |
408
- | `true` | HTTP status | `/auth/renew` was called and its `Set-Cookie` headers were copied. |
274
+ | Property | Type / allowed values | Default | Description |
275
+ | ------------- | ----------------------------- | ----------------- | ------------------------------------------------------------------------- |
276
+ | `sessionName` | Valid cookie name | `"__ses"` | Contains the opaque token. This is the only authenticating cookie. |
277
+ | `cacheName` | Valid cookie name | `"__cac"` | Contains the optional signed short cache. |
278
+ | `renewName` | Valid cookie name | `"__ren"` | Contains the untrusted `renew_at` Unix timestamp. |
279
+ | `domain` | `string` | Browser host only | Optional cookie domain. |
280
+ | `path` | String beginning with `/` | `"/"` | Cookie path. |
281
+ | `sameSite` | `"Strict" \| "Lax" \| "None"` | `"Lax"` | Browser SameSite policy. |
282
+ | `secure` | `boolean` | `true` | Requires HTTPS when enabled. Set `false` only for local HTTP development. |
409
283
 
410
- The adapter forwards only the session cookie and the client/origin headers required for session, host, and CSRF validation. If `Origin` is missing and both `X-Forwarded-Proto` and `X-Forwarded-Host` were received, it reconstructs the public origin from those trusted proxy headers. It never derives public headers from the internal `NextRequest` URL. Other cookies, `Authorization`, and arbitrary request headers are never forwarded. Renewal rejects redirects and times out after five seconds. `renewUrl` must point to the application's trusted private API, and the deployment proxy must overwrite forwarded headers from untrusted clients.
284
+ All three cookies are always `HttpOnly` and expire with their respective server-side purpose. Their names must be unique.
411
285
 
412
- Protected API endpoints remain responsible for real authentication. The renewal marker is only a browser scheduling mechanism and never acts as a refresh token.
286
+ Cookie prefix rules are enforced:
413
287
 
414
- Apply the proxy only to protected application routes, or skip renewal handling for public routes before calling `nextAuth.renew`. Otherwise a missing cookie produces `status: 401`, which is correct for protected routes but not for login or public pages.
288
+ - `__Host-` requires `secure: true`, `path: "/"`, and no `domain`.
289
+ - `__Secure-` requires `secure: true`.
290
+ - `SameSite=None` requires `secure: true`.
415
291
 
416
- ## Configuration
292
+ SameSite values:
417
293
 
418
- ### Password
294
+ | Enum | Behavior |
295
+ | ---------- | ---------------------------------------------------------------------------- |
296
+ | `"Strict"` | Sends cookies only in same-site contexts. |
297
+ | `"Lax"` | Sends cookies in same-site contexts and eligible top-level safe navigations. |
298
+ | `"None"` | Allows cross-site cookie use and requires `secure: true`. |
419
299
 
420
- Argon2id is the default:
300
+ The resolved names are available through:
421
301
 
422
302
  ```ts
423
- password: {
424
- algorithm: "argon2id",
425
- }
303
+ auth.cookie.sessionName; // "__ses"
304
+ auth.cookie.cacheName; // "__cac"
305
+ auth.cookie.renewName; // "__ren"
426
306
  ```
427
307
 
428
- | Argon2id property | Default | Allowed |
429
- | --- | ---: | ---: |
430
- | `hashLength` | `32` | `16` to `64` |
431
- | `maxBytes` | `1024` | `1` to `1,048,576` |
432
- | `memoryCost` | `65,536` | `8,192` to `1,048,576` |
433
- | `parallelism` | `4` | `1` to `16` |
434
- | `timeCost` | `3` | `1` to `10` |
308
+ ### Signed cache
309
+
310
+ | Property | Type / allowed values | Default | Description |
311
+ | ----------- | ----------------------------------- | -------- | --------------------------------------------------------- |
312
+ | `cache.ttl` | Integer `1` to `session.ttl` | Disabled | Maximum cache lifetime in seconds. |
313
+ | `secret` | String with at least 32 UTF-8 bytes | — | Signs the cache with HMAC-SHA-256. Required with `cache`. |
314
+
315
+ The cache:
435
316
 
436
- Applications with existing bcrypt hashes must select bcrypt explicitly:
317
+ - is cryptographically bound to the opaque token and client identity;
318
+ - is accepted only for `GET` and `HEAD`;
319
+ - never extends the authoritative database session;
320
+ - is bypassed for unsafe methods, renewal, logout, WebSockets, and core calls;
321
+ - falls back to normal database authentication when absent, expired, malformed, or altered.
322
+
323
+ The cache is signed but not encrypted. Do not place passwords, password hashes, raw session tokens, or application secrets in account/session data.
324
+
325
+ <details>
326
+ <summary>Internal compact cache payload</summary>
437
327
 
438
328
  ```ts
439
- password: {
440
- algorithm: "bcrypt",
329
+ {
330
+ exp: cacheExpiresAt,
331
+ acc: {
332
+ id,
333
+ email,
334
+ name,
335
+ role,
336
+ status,
337
+ timezone,
338
+ usr: { id, role, status },
339
+ },
340
+ ses: {
341
+ id,
342
+ client: { ip, agent, platform },
343
+ created_at,
344
+ exp: expiresAt,
345
+ ren: renewAt,
346
+ },
441
347
  }
442
348
  ```
443
349
 
444
- | bcrypt property | Default | Allowed |
445
- | --- | ---: | ---: |
446
- | `maxBytes` | `72` | `1` to `72` |
447
- | `rounds` | `12` | `4` to `31` |
448
- | `verifyMaxBytes` | `72` | `maxBytes` to `1,048,576` |
350
+ `session.account_id` is reconstructed from `acc.id` after signature validation.
449
351
 
450
- The selected algorithm is used for both hashing and verification. The package never detects algorithms, migrates hashes, rehashes passwords, or falls back to another algorithm.
352
+ </details>
451
353
 
452
- ### Session
354
+ ### Prisma adapter
453
355
 
454
356
  ```ts
455
- session: {
456
- maxLifetime: 30 * 24 * 60 * 60,
457
- renewInterval: 24 * 60 * 60,
458
- ttl: 7 * 24 * 60 * 60,
459
- validation: ["agent"],
460
- }
357
+ const db = createPrismaAdapter({
358
+ client: prisma,
359
+ config: {
360
+ session: {
361
+ table: "account_sessions",
362
+ relations: {
363
+ account: "account",
364
+ user: "user",
365
+ },
366
+ },
367
+ access: {
368
+ account: {
369
+ allowedRoles: ["OWNER", "ADMIN"],
370
+ allowedStatuses: ["ACTIVE"],
371
+ },
372
+ user: {
373
+ allowedRoles: ["ADMIN"],
374
+ allowedStatuses: ["ACTIVE"],
375
+ },
376
+ },
377
+ },
378
+ });
461
379
  ```
462
380
 
463
- Time values are seconds.
381
+ | Property | Type / allowed values | Required | Default | Description |
382
+ | --------------------------------------- | ------------------------------- | :-----------------------------: | -------------------- | --------------------------------------------------------------------- |
383
+ | `client` | Generated Prisma client | ✅ | — | Prisma client containing the session delegate. |
384
+ | `config.session` | Session model configuration | ❌ | Conventional names | Groups the Prisma delegate and relation names. |
385
+ | `config.session.table` | Compatible Prisma delegate name | Only without `account_sessions` | `"account_sessions"` | Delegate used to store sessions. This is not the physical table name. |
386
+ | `config.session.relations.account` | Non-empty `string` | ❌ | `"account"` | Account relation field on the session model. |
387
+ | `config.session.relations.user` | Non-empty `string` | ❌ | `"user"` | User relation field nested inside the account relation. |
388
+ | `config.access.account.allowedStatuses` | Non-empty unique `string[]` | ✅ | — | Account statuses allowed to authenticate. |
389
+ | `config.access.account.allowedRoles` | Non-empty unique `string[]` | ❌ | All roles | Account roles allowed to authenticate. |
390
+ | `config.access.user.allowedStatuses` | Non-empty unique `string[]` | ✅ | — | User statuses allowed to authenticate. |
391
+ | `config.access.user.allowedRoles` | Non-empty unique `string[]` | ❌ | All roles | User roles allowed to authenticate. |
464
392
 
465
- | Property | Default | Allowed | Purpose |
466
- | --- | ---: | ---: | --- |
467
- | `maxLifetime` | `2,592,000` | `ttl` to `31,536,000` | Maximum lifetime from the original login, regardless of activity. |
468
- | `renewInterval` | `86,400` | `1` to `ttl - 1` | Minimum interval before renewal is due. |
469
- | `ttl` | `604,800` | `60` to `31,536,000` | Sliding inactivity lifetime. |
470
- | `validation` | `["agent"]` | Unique `agent`, `ip`, `platform` fields | Exact client fields compared on every validation. |
393
+ Status and role values are deliberately dynamic. The package does not define application-specific enums.
394
+ Every configured account and user condition must match. Omitting `allowedRoles` accepts every role, but both `allowedStatuses` lists remain required.
471
395
 
472
- The authoritative limits are derived from the immutable creation time and the last successful renewal:
396
+ ### Next.js adapter
397
+
398
+ ```ts
399
+ import { createNextAuth } from "@gauts/auth/next";
400
+
401
+ export const nextAuth = createNextAuth({
402
+ renewUrl: `${process.env.NEXT_PRIVATE_API_URL}/auth/renew`,
403
+ });
404
+ ```
405
+
406
+ | Property | Type / allowed values | Required | Default | Description |
407
+ | -------------------- | -------------------------------- | :------: | --------- | --------------------------------------------- |
408
+ | `renewUrl` | Absolute `http:` or `https:` URL | ✅ | — | Trusted private API renewal endpoint. |
409
+ | `cookie.sessionName` | Valid cookie name | ❌ | `"__ses"` | Session cookie read and forwarded to the API. |
410
+ | `cookie.renewName` | Valid cookie name | ❌ | `"__ren"` | Renewal scheduling cookie read by Next.js. |
411
+
412
+ ## Session flow
413
+
414
+ ### Login
473
415
 
474
416
  ```text
475
- maxExpiresAt = created_at + maxLifetime
476
- expires_at = min(now + ttl, maxExpiresAt)
477
- renew_at = min((updated_at ?? created_at) + renewInterval, maxExpiresAt)
417
+ credentials accepted
418
+ -> generate 256-bit opaque token
419
+ -> store SHA-256 token hash in DB
420
+ -> load current account and user
421
+ -> apply configured access rules
422
+ -> write __ses
423
+ -> write __ren
424
+ -> optionally write __cac
478
425
  ```
479
426
 
480
- `maxLifetime` must be greater than or equal to `ttl`. It is enforced by the session core and requires a new login after the limit is reached; no additional database column is required.
427
+ Only the raw browser token authenticates. The database stores only its SHA-256 hash.
481
428
 
482
- ### Cookie
429
+ ### Protected `GET` or `HEAD`
483
430
 
484
- ```ts
485
- cookie: {
486
- cacheName: "__cac",
487
- domain: undefined,
488
- name: "__sec",
489
- path: "/",
490
- renewName: "__ren",
491
- sameSite: "Lax",
492
- secure: true,
493
- }
431
+ ```text
432
+ session token
433
+ -> valid signed cache?
434
+ -> yes: expose cached account, user, and session
435
+ -> no: validate through DB and create a fresh cache
436
+ ```
437
+
438
+ ### Unsafe request
439
+
440
+ ```text
441
+ session token
442
+ -> SHA-256 hash
443
+ -> indexed DB lookup
444
+ -> validate expiry, revocation, account, user, and client
445
+ -> clear short cache
446
+ -> continue
494
447
  ```
495
448
 
496
- `HttpOnly` is always enabled.
449
+ ### Renewal
497
450
 
498
- - The session cookie contains only the stable opaque token and expires with the database session.
499
- - The cache cookie contains the signed snapshot and expires after `cache.ttl`.
500
- - The renewal cookie contains the authoritative `renew_at` as Unix seconds and expires with the session cookie.
501
- - The renewal timestamp is an untrusted scheduling hint. It never authenticates or extends a session.
502
- - Cookie names default to `__sec`, `__cac`, and `__ren`.
503
- - All three names must be valid and unique.
451
+ ```text
452
+ Next reads __ren
453
+ -> future timestamp: no API request
454
+ -> missing, invalid, or due: POST /auth/renew
455
+ -> API validates through DB
456
+ -> update expires_at when renewal is due
457
+ -> Set-Cookie with the same token, new renewAt, and fresh cache
458
+ ```
504
459
 
505
- - `__Host-` requires `secure: true`, `path: "/"`, and no domain.
506
- - `__Secure-` requires `secure: true`.
507
- - `SameSite=None` requires `secure: true`.
508
- - Local HTTP development requires `secure: false`.
460
+ `auth.session.resolve()` is always DB-backed and read-only. Only explicit renewal updates database expiry.
509
461
 
510
- ### Signed session cache
462
+ ## Prisma schema
511
463
 
512
- The cache is disabled by default. Enable it explicitly:
464
+ The Prisma adapter resolves:
513
465
 
514
- ```ts
515
- secret: process.env.AUTH_SECRET,
516
- cache: {
517
- ttl: 60,
466
+ ```text
467
+ account_sessions -> account -> user
468
+ ```
469
+
470
+ The following is a complete MySQL/MariaDB example. Merge the required fields and relations into existing account and user models when applicable.
471
+
472
+ ```prisma
473
+ model users {
474
+ id String @id @default(uuid()) @db.VarChar(255)
475
+ role String @db.VarChar(255)
476
+ status String @db.VarChar(255)
477
+
478
+ accounts user_accounts[]
479
+ }
480
+
481
+ model user_accounts {
482
+ id String @id @default(uuid()) @db.VarChar(255)
483
+ user_id String @db.VarChar(255)
484
+ email String @db.VarChar(255)
485
+ name String @db.VarChar(255)
486
+ role String @db.VarChar(255)
487
+ status String @db.VarChar(255)
488
+ timezone String? @db.VarChar(255)
489
+
490
+ user users @relation(fields: [user_id], references: [id], onDelete: Cascade)
491
+ sessions account_sessions[]
492
+
493
+ @@index([user_id])
494
+ }
495
+
496
+ model account_sessions {
497
+ id String @id @default(uuid()) @db.VarChar(255)
498
+ account_id String @db.VarChar(255)
499
+ token_hash String @unique @db.VarChar(64)
500
+ ip String? @db.VarChar(45)
501
+ platform String? @db.VarChar(255)
502
+ agent String? @db.Text
503
+ expires_at DateTime @db.Timestamp(0)
504
+ revoked_at DateTime? @db.Timestamp(0)
505
+ created_at DateTime @default(now()) @db.Timestamp(0)
506
+ updated_at DateTime? @db.Timestamp(0)
507
+
508
+ account user_accounts @relation(fields: [account_id], references: [id], onDelete: Cascade)
509
+
510
+ @@index([account_id])
511
+ @@index([expires_at])
512
+ @@index([revoked_at])
518
513
  }
519
514
  ```
520
515
 
521
- `ttl` is measured in seconds and must be an integer from `1` through the configured session TTL. The secret must contain at least 32 bytes and should be generated from a cryptographically secure source.
516
+ Required fields and relation names:
517
+
518
+ | Path | Required fields |
519
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
520
+ | Session model | `id`, `account_id`, `token_hash`, `ip`, `platform`, `agent`, `expires_at`, `revoked_at`, `created_at`, `updated_at` |
521
+ | `account` relation | `id`, `email`, `name`, `role`, `status`, `timezone` |
522
+ | `account.user` relation | `id`, `role`, `status` |
523
+
524
+ The relation names default to `account` and `user`. Configure `session.relations` when the application uses different field names. Their inverse relation names may differ. Application models may add fields, indexes, defaults, and relations. Role and status fields may use Prisma enums.
522
525
 
523
- The cache payload:
526
+ Keep `agent` large enough for the complete User-Agent. Use provider-compatible native annotations when the database is not MySQL/MariaDB.
524
527
 
525
- - is signed with HMAC-SHA-256 using a domain-separated context;
526
- - is cryptographically bound to the opaque session token;
527
- - contains the resolved session, account, user, and its own expiry;
528
- - compares the same configured IP, User-Agent, and platform fields on every hit;
529
- - never extends the authoritative session expiry;
530
- - is accepted only for `GET` and `HEAD` requests.
528
+ `config.session.table` is a Prisma client delegate name. A model named `AdminSession` mapped with `@@map("account_sessions")` normally uses the `adminSession` delegate.
531
529
 
532
- An absent, expired, malformed, altered, token-mismatched, or client-mismatched cache is a cache miss. The adapter then performs normal database authentication. A real configured client mismatch is therefore still detected and revoked by the database session service.
530
+ Create and run migrations through the application's Prisma workflow. The package never manages migrations.
533
531
 
534
- Unsafe methods always query the database. Renewal and logout also query the database directly. `auth.session.resolve()` never uses the browser cache, which keeps WebSocket and non-HTTP integrations DB-backed.
532
+ ## Hono adapter
535
533
 
536
- ### Trusted client IP
534
+ ### Request values
535
+
536
+ `auth.requireSession` sets fully typed values on the Hono context:
537
537
 
538
538
  ```ts
539
- getIp: (c) => getTrustedClientIp(c);
539
+ const account = c.get("account");
540
+ const session = c.get("session");
541
+ const user = c.get("user");
540
542
  ```
541
543
 
542
- Only the application knows which proxy and forwarding header are trusted. The package normalizes the returned value but never chooses `X-Forwarded-For`, `CF-Connecting-IP`, or a socket address itself.
544
+ ```ts
545
+ type AuthAccount = {
546
+ email: string;
547
+ id: string;
548
+ name: string;
549
+ role: string;
550
+ status: string;
551
+ timezone: string | null;
552
+ user: AuthUser;
553
+ };
543
554
 
544
- The Hono adapter reads:
555
+ type AuthUser = {
556
+ id: string;
557
+ role: string;
558
+ status: string;
559
+ };
545
560
 
546
- ```ts
547
- type SessionClientInput = {
548
- agent?: string | null;
549
- ip?: string | null;
550
- platform?: string | null;
561
+ type Session = {
562
+ account_id: string;
563
+ client: {
564
+ agent: string | null;
565
+ ip: string | null;
566
+ platform: string | null;
567
+ };
568
+ created_at: Date;
569
+ expires_at: Date;
570
+ id: string;
571
+ renew_at: Date;
551
572
  };
552
573
  ```
553
574
 
554
- - IPv4, IPv4-mapped IPv6, and IPv6 are canonicalized.
555
- - Invalid or empty IP values become `null`.
556
- - Platform comes from `Sec-CH-UA-Platform`, is normalized, and is limited to 255 characters.
557
- - User-Agent comes from `User-Agent` and is stored in full.
558
- - No GeoIP, DNS, country, or external lookup is performed.
559
- - Every field selected in `session.validation` is required during creation and validation. Missing or invalid configured fields never match.
575
+ Only `account_id` is persisted in the session row. Current account and user data are loaded through the database relation and never copied into the table.
576
+
577
+ ### Methods
560
578
 
561
- ## Database adapter
579
+ | Method | Purpose |
580
+ | --------------------------------------------- | -------------------------------------------------------------------------- |
581
+ | `auth.createSession({ account_id, context })` | Creates the DB session and writes the browser cookies. |
582
+ | `auth.resolveSession(context)` | Resolves a request and returns account, user, and session. |
583
+ | `auth.renewSession(context)` | Performs DB validation, renews when due, and writes authoritative cookies. |
584
+ | `auth.revokeSession(context)` | Revokes the current DB session and clears cookies. |
585
+ | `auth.clearSession(context)` | Clears browser cookies without revoking the DB session. |
586
+ | `auth.getToken(context)` | Returns the validated opaque token from the request cookie. |
587
+ | `auth.requireSession` | Hono middleware that authenticates and populates the context. |
562
588
 
563
- The core depends only on the exported `DbAdapter` contract. It does not import Prisma or depend on a specific ORM. `createPrismaAdapter` is the Prisma implementation exposed through `@gauts/auth/prisma`; other ORM implementations can use their own package subpath and factory name.
589
+ `requireSession` authenticates only. Application-specific route permissions remain the application's responsibility.
564
590
 
565
- Default Prisma delegate (`account_sessions`):
591
+ ### Core and adapter composition
592
+
593
+ `createHonoAuth()` is the normal entry point. Use separate composition only when the same core instance is required outside Hono:
566
594
 
567
595
  ```ts
568
- import { createPrismaAdapter } from "@gauts/auth/prisma";
596
+ import { createAuth } from "@gauts/auth";
597
+ import { createHonoAdapter } from "@gauts/auth/hono";
569
598
 
570
- const db = createPrismaAdapter({
571
- client: prisma,
572
- config: {
573
- account: {
574
- status: ["ACTIVE"],
575
- },
576
- user: {
577
- status: ["ACTIVE"],
578
- },
579
- },
599
+ const core = createAuth({ db });
600
+ const hono = createHonoAdapter({
601
+ auth: core,
580
602
  });
581
603
  ```
582
604
 
583
- Custom compatible Prisma delegate:
605
+ ## Next.js adapter
606
+
607
+ The Next.js adapter schedules renewal; it does not authenticate pages or API requests.
584
608
 
585
609
  ```ts
586
- const db = createPrismaAdapter({
587
- client: prisma,
588
- config: {
589
- account: {
590
- status: ["ACTIVE"],
591
- },
592
- table: "admin_sessions",
593
- user: {
594
- status: ["ACTIVE"],
595
- },
596
- },
597
- });
598
- ```
610
+ import type { NextRequest } from "next/server";
611
+ import { NextResponse } from "next/server";
599
612
 
600
- Access rules:
613
+ export const proxy = async (request: NextRequest) => {
614
+ const response = NextResponse.next();
615
+ const renewal = await nextAuth.renew({ request, response });
601
616
 
602
- ```ts
603
- const db = createPrismaAdapter({
604
- client: prisma,
605
- config: {
606
- account: {
607
- status: ["ACTIVE", "PENDING"],
608
- },
609
- user: {
610
- role: ["ADMIN"],
611
- status: ["ACTIVE", "PENDING"],
612
- },
613
- },
614
- });
617
+ if (renewal.status === 401) {
618
+ return NextResponse.redirect(new URL("/auth/login", request.url));
619
+ }
620
+
621
+ if (renewal.status !== null && renewal.status >= 500) {
622
+ return NextResponse.redirect(new URL("/maintenance", request.url));
623
+ }
624
+
625
+ return renewal.response;
626
+ };
615
627
  ```
616
628
 
617
- Account and user status lists are required because the package cannot know which application-specific values grant access. Roles are unrestricted unless configured. Every list must contain unique non-empty strings. A role rule controls authentication for the entire application; route-level authorization remains the application's responsibility.
629
+ Result values:
618
630
 
619
- The configured arrays are access allowlists, not declarations of every enum value that exists in the application.
631
+ | `attempted` | `status` | Meaning |
632
+ | :---------: | ----------: | --------------------------------------------------------------- |
633
+ | `false` | `null` | Session token exists and renewal is not due. |
634
+ | `false` | `401` | Session token is missing or malformed; no API request occurred. |
635
+ | `true` | HTTP status | The renewal endpoint was called and returned this status. |
620
636
 
621
- Custom database adapters implement:
637
+ The adapter copies every returned `Set-Cookie` header to the browser response. It forwards only the session cookie and controlled client/origin headers required by the private API. Other cookies, authorization headers, and arbitrary headers are not forwarded.
622
638
 
623
- ```ts
624
- import type { DbAdapter } from "@gauts/auth";
639
+ `FORWARD_HEADERS` is exported from `@gauts/auth/next` for application fetchers that need the same controlled header list.
625
640
 
626
- const db = {
627
- create: async (session) => {},
628
- find: async ({ account_id, session_id }) => null,
629
- findActive: async ({ account_id, now }) => [],
630
- findToken: async (token_hash) => null,
631
- revoke: async ({ revoked_at, session_ids }) => {},
632
- updateExpiry: async ({ expires_at, session_id, updated_at }) => {},
633
- } satisfies DbAdapter;
634
- ```
641
+ When `Origin` is absent and trusted `X-Forwarded-Proto` and `X-Forwarded-Host` headers exist, the adapter reconstructs the public origin from them. It never derives a public origin from the internal Next.js request URL. The deployment proxy must overwrite forwarded headers received from untrusted clients.
635
642
 
636
- `findToken` must use the token hash and never accept or persist a raw token. It returns the current nested `account`, its `user`, and an `allowed` result derived from the adapter's access rules.
643
+ Apply renewal only to protected routes or skip public routes before calling `nextAuth.renew()`.
637
644
 
638
645
  ## Core session API
639
646
 
@@ -647,27 +654,46 @@ await auth.session.revokeToken(token);
647
654
  await auth.session.revokeAccount(account_id);
648
655
  ```
649
656
 
650
- - `resolve` performs read-only authentication.
651
- - `renew` validates and updates expiry only when due.
652
- - `list` returns active, non-expired sessions without token hashes.
653
- - revocation retains database history through `revoked_at`.
654
- - the package does not limit session count or delete historical rows; retention and cleanup belong to the application.
657
+ | Method | Behavior |
658
+ | --------------- | ------------------------------------------------------ |
659
+ | `create` | Creates a session and returns the raw token once. |
660
+ | `resolve` | Performs read-only DB authentication. |
661
+ | `renew` | Validates through DB and updates expiry only when due. |
662
+ | `list` | Returns active sessions without token hashes. |
663
+ | `revoke` | Revokes one session belonging to an account. |
664
+ | `revokeToken` | Revokes the session matching a raw token. |
665
+ | `revokeAccount` | Revokes every active session for an account. |
655
666
 
656
- ## Performance and cache policy
667
+ The package does not limit session count or delete historical rows. Retention and cleanup belong to the application.
657
668
 
658
- The package contains no Redis or in-process cache. The optional signed browser cache avoids shared infrastructure and works across API instances that use the same `AUTH_SECRET`.
669
+ ## Custom database adapter
659
670
 
660
- Without cache, each `requireSession` performs:
671
+ The core depends on `DbAdapter`, not Prisma:
661
672
 
662
- ```text
663
- 1 Prisma relation lookup by indexed account_sessions.token_hash
673
+ ```ts
674
+ import type { DbAdapter } from "@gauts/auth";
675
+
676
+ const db = {
677
+ create: async (session) => {},
678
+ find: async ({ account_id, session_id }) => null,
679
+ findActive: async ({ account_id, now }) => [],
680
+ findToken: async (token_hash) => null,
681
+ revoke: async ({ revoked_at, session_ids }) => {},
682
+ updateExpiry: async ({ expires_at, session_id, updated_at }) => {},
683
+ } satisfies DbAdapter;
664
684
  ```
665
685
 
666
- With a valid cache, `GET` and `HEAD` avoid that lookup until `cache.ttl` expires. Cache misses perform the normal indexed lookup and return a new cache cookie. Prisma loads current relations through the session query; the exact number of SQL statements depends on Prisma's configured relation load strategy.
686
+ `findToken` receives only the SHA-256 token hash. It must return the current nested account and user plus an `allowed` result. Raw tokens must never be persisted.
687
+
688
+ ## Performance
667
689
 
668
- Cookie caching has an explicit consistency tradeoff: revocation and account/user changes made elsewhere may remain visible to safe requests until the short cache expires. Unsafe methods never accept the cache, so writes observe current database state. A 60-second TTL limits the stale-read window to at most one minute.
690
+ The package contains no Redis or in-process cache.
669
691
 
670
- Logout clears the current browser's cache immediately. Cookies on another device cannot be remotely deleted, so immediate cross-device read revocation requires disabling the cache or using shared server-side state outside this package.
692
+ Without the optional browser cache, each `requireSession` performs an indexed database lookup through `account_sessions.token_hash`.
693
+
694
+ With a valid cache, `GET` and `HEAD` skip the lookup until `cache.ttl` expires. Unsafe methods always use current database state.
695
+
696
+ The tradeoff is explicit: revocation and account/user changes made elsewhere may remain visible to safe cached requests until the short TTL expires. A 60-second TTL limits this stale-read window to one minute. Disable cache when immediate read revocation is required.
671
697
 
672
698
  ## Errors
673
699
 
@@ -684,31 +710,29 @@ type AuthErrorCode =
684
710
 
685
711
  Use `isAuthError(error)` before reading `error.code`.
686
712
 
687
- The package does not choose HTTP responses. A typical application mapping is:
688
-
689
- | Code | Suggested HTTP status |
690
- | --- | ---: |
691
- | `AUTH_CONFIG_INVALID` | `500` during startup |
692
- | `PASSWORD_INPUT_INVALID` | `400` |
693
- | `SESSION_CLIENT_MISMATCH` | `403` |
694
- | `SESSION_DATA_INVALID` | `400` |
695
- | `SESSION_INVALID` | `401` |
696
- | `SESSION_NOT_FOUND` | `404` |
697
- | `DB_UNAVAILABLE` | `503` |
713
+ | Code | Suggested HTTP status | Meaning |
714
+ | ------------------------- | --------------------: | ----------------------------------------------------------------- |
715
+ | `AUTH_CONFIG_INVALID` | `500` | Invalid startup configuration. |
716
+ | `PASSWORD_INPUT_INVALID` | `400` | Password input violates configured limits. |
717
+ | `SESSION_CLIENT_MISMATCH` | `403` | A configured client field does not match; the session is revoked. |
718
+ | `SESSION_DATA_INVALID` | `400` | Invalid session or renewal data. |
719
+ | `SESSION_INVALID` | `401` | Missing, expired, revoked, or unknown session. |
720
+ | `SESSION_NOT_FOUND` | `404` | Requested session does not exist for the account. |
721
+ | `DB_UNAVAILABLE` | `503` | Database operation failed. Authentication fails closed. |
698
722
 
699
- Applications may use a different response policy, but database failures and invalid sessions must continue to fail closed.
723
+ The package throws typed errors but does not choose application HTTP responses.
700
724
 
701
725
  ## Security responsibilities
702
726
 
703
- The package provides session primitives, not a complete application security policy. Consuming applications remain responsible for:
727
+ The package provides authentication primitives, not a complete application security policy. Applications remain responsible for:
704
728
 
705
729
  - TLS and trusted-proxy configuration;
706
- - CSRF, CORS, and origin validation;
730
+ - CSRF, CORS, host, and origin validation;
707
731
  - login and renewal rate limiting;
708
732
  - equivalent password verification work for unknown accounts;
709
- - account status and authorization rules;
710
- - re-authentication for sensitive actions;
711
- - database migrations and cleanup;
733
+ - route roles and authorization;
734
+ - re-authentication for sensitive operations;
735
+ - database migrations and session cleanup;
712
736
  - never logging passwords, raw tokens, cookie headers, or password hashes.
713
737
 
714
- Exact IP/User-Agent/platform matching is defense in depth. It does not prevent every stolen-cookie replay scenario.
738
+ Exact IP, User-Agent, and platform validation are defense in depth. They do not prevent every stolen-cookie replay scenario.