@gauts/auth 0.4.0 → 0.5.1

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,301 +1,127 @@
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:
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.
72
55
 
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
- ```
56
+ ### Package entry points
81
57
 
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
- ```
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. |
92
64
 
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`.
65
+ ## Quick start
94
66
 
95
- ## Prisma schema contract
67
+ The Hono adapter does not create routes automatically. The application defines its own login, renewal, logout, and protected endpoints.
96
68
 
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.
162
-
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.
164
-
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.
166
-
167
- Create migrations through the consuming application's normal Prisma workflow. The package never creates or runs migrations.
168
-
169
- ## Hono quick start
170
-
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:
209
-
210
- The resolved cookie names are exposed on the auth instance, including defaults:
94
+ This uses the defaults:
211
95
 
212
- ```ts
213
- auth.cookie.sessionName; // "__ses"
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:
253
-
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
- };
112
+ import { auth } from "./auth.js";
113
+ import { DUMMY_PASSWORD_HASH } from "./password.js";
264
114
 
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
118
  const body = await c.req.json<{
295
119
  email: string;
296
120
  password: string;
297
121
  }>();
122
+
298
123
  const account = await findAccount(body.email);
124
+
299
125
  const passwordValid = await auth.password.verify({
300
126
  password: body.password,
301
127
  storedHash: account?.passwordHash ?? DUMMY_PASSWORD_HASH,
@@ -312,112 +138,78 @@ app.post("/auth/login", async (c) => {
312
138
 
313
139
  return c.json({ authenticated: true });
314
140
  });
315
- ```
316
141
 
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.
320
-
321
- ### Protect routes
322
-
323
- ```ts
324
- app.get("/account", auth.requireSession, (c) => {
325
- return c.json({
326
- account: c.get("account"),
327
- session_id: c.get("session").id,
328
- user: c.get("user"),
329
- });
330
- });
331
- ```
332
-
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.
334
-
335
- ### Renewal endpoint
336
-
337
- ```ts
338
142
  app.post("/auth/renew", async (c) => {
339
143
  await auth.renewSession(c);
340
144
  return c.body(null, 204);
341
145
  });
342
- ```
343
-
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.
345
-
346
- If renewal is not yet due, database expiry is not changed. The API still returns the authoritative session cookie, renewal marker, and cache.
347
146
 
348
- ### Logout
349
-
350
- ```ts
351
147
  app.post("/auth/logout", async (c) => {
352
148
  await auth.revokeSession(c);
353
149
  return c.body(null, 204);
354
150
  });
355
- ```
356
-
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.
358
-
359
- ## Next.js renewal adapter
360
-
361
- The Next.js adapter does not authenticate sessions. It calls the private API renewal URL when `renewAt` is missing, invalid, or due.
362
-
363
- ```ts
364
- import { createNextAuth } from "@gauts/auth/next";
365
151
 
366
- export const nextAuth = createNextAuth({
367
- renewUrl: `${process.env.NEXT_PRIVATE_API_URL}/auth/renew`,
152
+ app.get("/account", auth.requireSession, (c) => {
153
+ return c.json({
154
+ account: c.get("account"),
155
+ session: c.get("session"),
156
+ user: c.get("user"),
157
+ });
368
158
  });
369
159
  ```
370
160
 
371
- The controlled header list is also exported for application fetchers that need to follow the same forwarding policy:
161
+ 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.
162
+
163
+ ### 3. Enable the optional cache
372
164
 
373
165
  ```ts
374
- import { FORWARD_HEADERS } from "@gauts/auth/next";
166
+ export const auth = createHonoAuth({
167
+ cache: {
168
+ ttl: 60,
169
+ },
170
+ db,
171
+ secret: requiredEnv("AUTH_SECRET"),
172
+ });
375
173
  ```
376
174
 
377
- Use it in `proxy.ts` before returning the browser-facing response:
175
+ `AUTH_SECRET` must contain at least 32 high-entropy bytes. It stays in the API and is never shared with Next.js.
378
176
 
379
- ```ts
380
- import type { NextRequest } from "next/server";
381
- import { NextResponse } from "next/server";
177
+ `requiredEnv()` represents an application helper that returns a non-empty environment string or fails during startup.
382
178
 
383
- export const proxy = async (request: NextRequest) => {
384
- const response = NextResponse.next();
385
- const renewal = await nextAuth.renew({
386
- request,
387
- response,
388
- });
179
+ ## Configuration reference
389
180
 
390
- if (renewal.status === 401) {
391
- return NextResponse.redirect(new URL("/login", request.url));
392
- }
181
+ ### `createHonoAuth()`
393
182
 
394
- if (renewal.status !== null && renewal.status >= 500) {
395
- return NextResponse.redirect(new URL("/maintenance", request.url));
396
- }
183
+ | Property | Type / allowed values | Required | Default | Description |
184
+ | ---------- | --------------------- | :---------------------: | ----------------- | --------------------------------------------------------------------------------------------------- |
185
+ | `db` | `DbAdapter` | ✅ | — | Authoritative session persistence and account loading. |
186
+ | `getIp` | `HonoGetIp` | Only with IP validation | Omitted | Returns the client IP from a source trusted by the application. May be synchronous or asynchronous. |
187
+ | `password` | `PasswordConfig` | ❌ | Argon2id defaults | Password hashing and verification configuration. |
188
+ | `session` | `SessionConfig` | ❌ | Session defaults | Expiry, renewal, and client validation configuration. |
189
+ | `cookie` | `HonoCookieConfig` | ❌ | Cookie defaults | Names, domain, path, SameSite, and Secure settings. |
190
+ | `cache` | `{ ttl: number }` | ❌ | Disabled | Enables the short signed browser cache. |
191
+ | `secret` | `string` | When `cache` is enabled | — | HMAC secret for the signed cache. Minimum 32 UTF-8 bytes. |
397
192
 
398
- return renewal.response;
399
- };
193
+ ```ts
194
+ type HonoGetIp = (c: Context) => Promise<string | null | undefined> | string | null | undefined;
400
195
  ```
401
196
 
402
- Result semantics:
197
+ 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.
403
198
 
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. |
409
-
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.
411
-
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.
199
+ ### Password
413
200
 
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.
201
+ #### Argon2id
415
202
 
416
- ## Configuration
203
+ Argon2id is selected when `password.algorithm` is omitted or set to `"argon2id"`.
417
204
 
418
- ### Password
419
-
420
- Argon2id is the default:
205
+ | Property | Type / allowed values | Default | Description |
206
+ | ------------- | --------------------------- | -----------: | ----------------------------------------------------------------- |
207
+ | `algorithm` | `"argon2id"` | `"argon2id"` | Password algorithm used for both hashing and verification. |
208
+ | `hashLength` | Integer `16`–`64` | `32` | Output hash length in bytes. |
209
+ | `maxBytes` | Integer `1`–`1,048,576` | `1024` | Maximum UTF-8 password size accepted by hashing and verification. |
210
+ | `memoryCost` | Integer `8,192`–`1,048,576` | `65,536` | Argon2 memory cost in KiB. |
211
+ | `parallelism` | Integer `1`–`16` | `4` | Number of parallel lanes. |
212
+ | `timeCost` | Integer `1`–`10` | `3` | Number of Argon2 iterations. |
421
213
 
422
214
  ```ts
423
215
  password: {
@@ -425,15 +217,16 @@ password: {
425
217
  }
426
218
  ```
427
219
 
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` |
220
+ #### bcrypt
221
+
222
+ Applications with existing bcrypt hashes must select bcrypt explicitly.
435
223
 
436
- Applications with existing bcrypt hashes must select bcrypt explicitly:
224
+ | Property | Type / allowed values | Default | Description |
225
+ | ---------------- | -------------------------------------- | -------: | ------------------------------------------------------- |
226
+ | `algorithm` | `"bcrypt"` | Required | Selects bcrypt for both hashing and verification. |
227
+ | `maxBytes` | Integer `1`–`72` | `72` | Maximum UTF-8 size accepted for new passwords. |
228
+ | `rounds` | Integer `4`–`31` | `12` | bcrypt cost factor. |
229
+ | `verifyMaxBytes` | Integer from `maxBytes` to `1,048,576` | `72` | Maximum input accepted while verifying existing hashes. |
437
230
 
438
231
  ```ts
439
232
  password: {
@@ -441,95 +234,101 @@ password: {
441
234
  }
442
235
  ```
443
236
 
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` |
449
-
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.
237
+ The package does not detect algorithms, migrate hashes, rehash passwords, or fall back to another algorithm.
451
238
 
452
239
  ### Session
453
240
 
241
+ All time values are seconds.
242
+
243
+ | Property | Type / allowed values | Default | Description |
244
+ | --------------- | ----------------------------------------------- | --------------------: | ------------------------------------------------------------------------- |
245
+ | `maxLifetime` | Integer from `ttl` to `31,536,000` | `2,592,000` (30 days) | Maximum session lifetime from the original login, regardless of activity. |
246
+ | `renewInterval` | Integer `1` to `ttl - 1` | `86,400` (24 hours) | Minimum interval before sliding renewal is due. |
247
+ | `ttl` | Integer `60`–`31,536,000` | `604,800` (7 days) | Inactivity lifetime assigned at login and renewal. |
248
+ | `validation` | Unique array of `"agent"`, `"ip"`, `"platform"` | `["agent"]` | Client fields that must match the stored session exactly. |
249
+
454
250
  ```ts
455
251
  session: {
456
- maxLifetime: 30 * 24 * 60 * 60,
457
- renewInterval: 24 * 60 * 60,
458
- ttl: 7 * 24 * 60 * 60,
459
- validation: ["agent"],
252
+ maxLifetime: 60 * 60 * 24 * 30,
253
+ renewInterval: 60 * 60 * 24,
254
+ ttl: 60 * 60 * 24 * 7,
255
+ validation: ["agent", "ip"],
460
256
  }
461
257
  ```
462
258
 
463
- Time values are seconds.
259
+ Validation values:
464
260
 
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. |
261
+ | Enum | Source | Behavior |
262
+ | ------------ | ------------------------------------- | -------------------------------------------------------- |
263
+ | `"agent"` | Complete `User-Agent` header | Enabled by default. The complete value must match. |
264
+ | `"ip"` | Application-provided `getIp()` result | IPv4 and IPv6 are canonicalized before exact comparison. |
265
+ | `"platform"` | `Sec-CH-UA-Platform` header | Normalized and compared exactly. |
471
266
 
472
- The authoritative limits are derived from the immutable creation time and the last successful renewal:
267
+ Every selected field is required during session creation and validation. A mismatch revokes the database session.
268
+
269
+ Expiry is derived as follows:
473
270
 
474
271
  ```text
475
272
  maxExpiresAt = created_at + maxLifetime
476
273
  expires_at = min(now + ttl, maxExpiresAt)
477
- renew_at = min((updated_at ?? created_at) + renewInterval, maxExpiresAt)
274
+ renew_at = min((updated_at ?? created_at) + renewInterval, maxExpiresAt)
478
275
  ```
479
276
 
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.
481
-
482
- ### Cookie
277
+ ### Cookies
483
278
 
484
- ```ts
485
- cookie: {
486
- cacheName: "__cac",
487
- domain: undefined,
488
- path: "/",
489
- renewName: "__ren",
490
- sameSite: "Lax",
491
- secure: true,
492
- sessionName: "__ses",
493
- }
494
- ```
279
+ | Property | Type / allowed values | Default | Description |
280
+ | ------------- | ----------------------------- | ----------------- | ------------------------------------------------------------------------- |
281
+ | `sessionName` | Valid cookie name | `"__ses"` | Contains the opaque token. This is the only authenticating cookie. |
282
+ | `cacheName` | Valid cookie name | `"__cac"` | Contains the optional signed short cache. |
283
+ | `renewName` | Valid cookie name | `"__ren"` | Contains the untrusted `renew_at` Unix timestamp. |
284
+ | `domain` | `string` | Browser host only | Optional cookie domain. |
285
+ | `path` | String beginning with `/` | `"/"` | Cookie path. |
286
+ | `sameSite` | `"Strict" \| "Lax" \| "None"` | `"Lax"` | Browser SameSite policy. |
287
+ | `secure` | `boolean` | `true` | Requires HTTPS when enabled. Set `false` only for local HTTP development. |
495
288
 
496
- `HttpOnly` is always enabled.
289
+ All three cookies are always `HttpOnly` and expire with their respective server-side purpose. Their names must be unique.
497
290
 
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 `__ses`, `__cac`, and `__ren`.
503
- - All three names must be valid and unique.
291
+ Cookie prefix rules are enforced:
504
292
 
505
- - `__Host-` requires `secure: true`, `path: "/"`, and no domain.
293
+ - `__Host-` requires `secure: true`, `path: "/"`, and no `domain`.
506
294
  - `__Secure-` requires `secure: true`.
507
295
  - `SameSite=None` requires `secure: true`.
508
- - Local HTTP development requires `secure: false`.
509
296
 
510
- ### Signed session cache
297
+ SameSite values:
511
298
 
512
- The cache is disabled by default. Enable it explicitly:
299
+ | Enum | Behavior |
300
+ | ---------- | ---------------------------------------------------------------------------- |
301
+ | `"Strict"` | Sends cookies only in same-site contexts. |
302
+ | `"Lax"` | Sends cookies in same-site contexts and eligible top-level safe navigations. |
303
+ | `"None"` | Allows cross-site cookie use and requires `secure: true`. |
304
+
305
+ The resolved names are available through:
513
306
 
514
307
  ```ts
515
- secret: process.env.AUTH_SECRET,
516
- cache: {
517
- ttl: 60,
518
- }
308
+ auth.cookie.sessionName; // "__ses"
309
+ auth.cookie.cacheName; // "__cac"
310
+ auth.cookie.renewName; // "__ren"
519
311
  ```
520
312
 
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.
313
+ ### Signed cache
314
+
315
+ | Property | Type / allowed values | Default | Description |
316
+ | ----------- | ----------------------------------- | -------- | --------------------------------------------------------- |
317
+ | `cache.ttl` | Integer `1` to `session.ttl` | Disabled | Maximum cache lifetime in seconds. |
318
+ | `secret` | String with at least 32 UTF-8 bytes | — | Signs the cache with HMAC-SHA-256. Required with `cache`. |
522
319
 
523
- The cache payload:
320
+ The cache:
524
321
 
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.
322
+ - is cryptographically bound to the opaque token and client identity;
323
+ - is accepted only for `GET` and `HEAD`;
324
+ - never extends the authoritative database session;
325
+ - is bypassed for unsafe methods, renewal, logout, WebSockets, and core calls;
326
+ - falls back to normal database authentication when absent, expired, malformed, or altered.
531
327
 
532
- The signed value uses a compact internal payload:
328
+ The cache is signed but not encrypted. Do not place passwords, password hashes, raw session tokens, or application secrets in account/session data.
329
+
330
+ <details>
331
+ <summary>Internal compact cache payload</summary>
533
332
 
534
333
  ```ts
535
334
  {
@@ -553,113 +352,311 @@ The signed value uses a compact internal payload:
553
352
  }
554
353
  ```
555
354
 
556
- `account_id` is not duplicated in the cookie. After signature validation, the adapter rebuilds `session.account_id` from `acc.id`, so the public `session` object is unchanged. The cache is signed but not encrypted; never place passwords, hashes, raw session tokens, or application secrets in it.
557
-
558
- 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.
355
+ `session.account_id` is reconstructed from `acc.id` after signature validation.
559
356
 
560
- 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.
357
+ </details>
561
358
 
562
- ### Trusted client IP
359
+ ### Prisma adapter
563
360
 
564
361
  ```ts
565
- getIp: (c) => getTrustedClientIp(c);
362
+ const db = createPrismaAdapter({
363
+ client: prisma,
364
+ // config: {
365
+ // session: {
366
+ // table: "account_sessions",
367
+ // relations: {
368
+ // account: "account",
369
+ // user: "user",
370
+ // },
371
+ // },
372
+ // access: {
373
+ // account: {
374
+ // allowedRoles: ["OWNER", "ADMIN"],
375
+ // allowedStatuses: ["ACTIVE"],
376
+ // },
377
+ // user: {
378
+ // allowedRoles: ["ADMIN"],
379
+ // allowedStatuses: ["ACTIVE"],
380
+ // },
381
+ // },
382
+ // },
383
+ });
566
384
  ```
567
385
 
568
- 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.
386
+ | Property | Type / allowed values | Required | Default | Description |
387
+ | --------------------------------------- | ------------------------------- | :-----------------------------: | -------------------- | --------------------------------------------------------------------- |
388
+ | `client` | Generated Prisma client | ✅ | — | Prisma client containing the session delegate. |
389
+ | `config.session` | Session model configuration | ❌ | Conventional names | Groups the Prisma delegate and relation names. |
390
+ | `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. |
391
+ | `config.session.relations.account` | Non-empty `string` | ❌ | `"account"` | Account relation field on the session model. |
392
+ | `config.session.relations.user` | Non-empty `string` | ❌ | `"user"` | User relation field nested inside the account relation. |
393
+ | `config.access.account.allowedStatuses` | Non-empty unique `string[]` | ✅ | — | Account statuses allowed to authenticate. |
394
+ | `config.access.account.allowedRoles` | Non-empty unique `string[]` | ❌ | All roles | Account roles allowed to authenticate. |
395
+ | `config.access.user.allowedStatuses` | Non-empty unique `string[]` | ✅ | — | User statuses allowed to authenticate. |
396
+ | `config.access.user.allowedRoles` | Non-empty unique `string[]` | ❌ | All roles | User roles allowed to authenticate. |
569
397
 
570
- The Hono adapter reads:
398
+ Status and role values are deliberately dynamic. The package does not define application-specific enums.
399
+ Every configured account and user condition must match. Omitting `allowedRoles` accepts every role, but both `allowedStatuses` lists remain required.
400
+
401
+ ### Next.js adapter
571
402
 
572
403
  ```ts
573
- type SessionClientInput = {
574
- agent?: string | null;
575
- ip?: string | null;
576
- platform?: string | null;
577
- };
404
+ import { createNextAuth } from "@gauts/auth/next";
405
+
406
+ export const nextAuth = createNextAuth({
407
+ renewUrl: `${process.env.NEXT_PRIVATE_API_URL}/auth/renew`,
408
+ });
409
+ ```
410
+
411
+ | Property | Type / allowed values | Required | Default | Description |
412
+ | -------------------- | -------------------------------- | :------: | --------- | --------------------------------------------- |
413
+ | `renewUrl` | Absolute `http:` or `https:` URL | ✅ | — | Trusted private API renewal endpoint. |
414
+ | `cookie.sessionName` | Valid cookie name | ❌ | `"__ses"` | Session cookie read and forwarded to the API. |
415
+ | `cookie.renewName` | Valid cookie name | ❌ | `"__ren"` | Renewal scheduling cookie read by Next.js. |
416
+
417
+ ## Session flow
418
+
419
+ ### Login
420
+
421
+ ```text
422
+ credentials accepted
423
+ -> generate 256-bit opaque token
424
+ -> store SHA-256 token hash in DB
425
+ -> load current account and user
426
+ -> apply configured access rules
427
+ -> write __ses
428
+ -> write __ren
429
+ -> optionally write __cac
578
430
  ```
579
431
 
580
- - IPv4, IPv4-mapped IPv6, and IPv6 are canonicalized.
581
- - Invalid or empty IP values become `null`.
582
- - Platform comes from `Sec-CH-UA-Platform`, is normalized, and is limited to 255 characters.
583
- - User-Agent comes from `User-Agent` and is stored in full.
584
- - No GeoIP, DNS, country, or external lookup is performed.
585
- - Every field selected in `session.validation` is required during creation and validation. Missing or invalid configured fields never match.
432
+ Only the raw browser token authenticates. The database stores only its SHA-256 hash.
433
+
434
+ ### Protected `GET` or `HEAD`
435
+
436
+ ```text
437
+ session token
438
+ -> valid signed cache?
439
+ -> yes: expose cached account, user, and session
440
+ -> no: validate through DB and create a fresh cache
441
+ ```
442
+
443
+ ### Unsafe request
444
+
445
+ ```text
446
+ session token
447
+ -> SHA-256 hash
448
+ -> indexed DB lookup
449
+ -> validate expiry, revocation, account, user, and client
450
+ -> clear short cache
451
+ -> continue
452
+ ```
453
+
454
+ ### Renewal
455
+
456
+ ```text
457
+ Next reads __ren
458
+ -> future timestamp: no API request
459
+ -> missing, invalid, or due: POST /auth/renew
460
+ -> API validates through DB
461
+ -> update expires_at when renewal is due
462
+ -> Set-Cookie with the same token, new renewAt, and fresh cache
463
+ ```
464
+
465
+ `auth.session.resolve()` is always DB-backed and read-only. Only explicit renewal updates database expiry.
466
+
467
+ ## Prisma schema
468
+
469
+ The Prisma adapter resolves:
470
+
471
+ ```text
472
+ account_sessions -> account -> user
473
+ ```
474
+
475
+ The following is a complete MySQL/MariaDB example. Merge the required fields and relations into existing account and user models when applicable.
476
+
477
+ ```prisma
478
+ model users {
479
+ id String @id @default(uuid()) @db.VarChar(255)
480
+ role String @db.VarChar(255)
481
+ status String @db.VarChar(255)
482
+
483
+ accounts user_accounts[]
484
+ }
485
+
486
+ model user_accounts {
487
+ id String @id @default(uuid()) @db.VarChar(255)
488
+ user_id String @db.VarChar(255)
489
+ email String @db.VarChar(255)
490
+ name String @db.VarChar(255)
491
+ role String @db.VarChar(255)
492
+ status String @db.VarChar(255)
493
+ timezone String? @db.VarChar(255)
494
+
495
+ user users @relation(fields: [user_id], references: [id], onDelete: Cascade)
496
+ sessions account_sessions[]
497
+
498
+ @@index([user_id])
499
+ }
500
+
501
+ model account_sessions {
502
+ id String @id @default(uuid()) @db.VarChar(255)
503
+ account_id String @db.VarChar(255)
504
+ token_hash String @unique @db.VarChar(64)
505
+ ip String? @db.VarChar(45)
506
+ platform String? @db.VarChar(255)
507
+ agent String? @db.Text
508
+ expires_at DateTime @db.Timestamp(0)
509
+ revoked_at DateTime? @db.Timestamp(0)
510
+ created_at DateTime @default(now()) @db.Timestamp(0)
511
+ updated_at DateTime? @db.Timestamp(0)
512
+
513
+ account user_accounts @relation(fields: [account_id], references: [id], onDelete: Cascade)
514
+
515
+ @@index([account_id])
516
+ @@index([expires_at])
517
+ @@index([revoked_at])
518
+ }
519
+ ```
520
+
521
+ Required fields and relation names:
522
+
523
+ | Path | Required fields |
524
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
525
+ | Session model | `id`, `account_id`, `token_hash`, `ip`, `platform`, `agent`, `expires_at`, `revoked_at`, `created_at`, `updated_at` |
526
+ | `account` relation | `id`, `email`, `name`, `role`, `status`, `timezone` |
527
+ | `account.user` relation | `id`, `role`, `status` |
528
+
529
+ 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.
530
+
531
+ Keep `agent` large enough for the complete User-Agent. Use provider-compatible native annotations when the database is not MySQL/MariaDB.
586
532
 
587
- ## Database adapter
533
+ `config.session.table` is a Prisma client delegate name. A model named `AdminSession` mapped with `@@map("account_sessions")` normally uses the `adminSession` delegate.
588
534
 
589
- 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.
535
+ Create and run migrations through the application's Prisma workflow. The package never manages migrations.
590
536
 
591
- Default Prisma delegate (`account_sessions`):
537
+ ## Hono adapter
538
+
539
+ ### Request values
540
+
541
+ `auth.requireSession` sets fully typed values on the Hono context:
592
542
 
593
543
  ```ts
594
- import { createPrismaAdapter } from "@gauts/auth/prisma";
544
+ const account = c.get("account");
545
+ const session = c.get("session");
546
+ const user = c.get("user");
547
+ ```
595
548
 
596
- const db = createPrismaAdapter({
597
- client: prisma,
598
- config: {
599
- account: {
600
- status: ["ACTIVE"],
601
- },
602
- user: {
603
- status: ["ACTIVE"],
604
- },
605
- },
606
- });
549
+ ```ts
550
+ type AuthAccount = {
551
+ email: string;
552
+ id: string;
553
+ name: string;
554
+ role: string;
555
+ status: string;
556
+ timezone: string | null;
557
+ user: AuthUser;
558
+ };
559
+
560
+ type AuthUser = {
561
+ id: string;
562
+ role: string;
563
+ status: string;
564
+ };
565
+
566
+ type Session = {
567
+ account_id: string;
568
+ client: {
569
+ agent: string | null;
570
+ ip: string | null;
571
+ platform: string | null;
572
+ };
573
+ created_at: Date;
574
+ expires_at: Date;
575
+ id: string;
576
+ renew_at: Date;
577
+ };
607
578
  ```
608
579
 
609
- Custom compatible Prisma delegate:
580
+ 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.
581
+
582
+ ### Methods
583
+
584
+ | Method | Purpose |
585
+ | --------------------------------------------- | -------------------------------------------------------------------------- |
586
+ | `auth.createSession({ account_id, context })` | Creates the DB session and writes the browser cookies. |
587
+ | `auth.resolveSession(context)` | Resolves a request and returns account, user, and session. |
588
+ | `auth.renewSession(context)` | Performs DB validation, renews when due, and writes authoritative cookies. |
589
+ | `auth.revokeSession(context)` | Revokes the current DB session and clears cookies. |
590
+ | `auth.clearSession(context)` | Clears browser cookies without revoking the DB session. |
591
+ | `auth.getToken(context)` | Returns the validated opaque token from the request cookie. |
592
+ | `auth.requireSession` | Hono middleware that authenticates and populates the context. |
593
+
594
+ `requireSession` authenticates only. Application-specific route permissions remain the application's responsibility.
595
+
596
+ ### Core and adapter composition
597
+
598
+ `createHonoAuth()` is the normal entry point. Use separate composition only when the same core instance is required outside Hono:
610
599
 
611
600
  ```ts
612
- const db = createPrismaAdapter({
613
- client: prisma,
614
- config: {
615
- account: {
616
- status: ["ACTIVE"],
617
- },
618
- table: "admin_sessions",
619
- user: {
620
- status: ["ACTIVE"],
621
- },
622
- },
601
+ import { createAuth } from "@gauts/auth";
602
+ import { createHonoAdapter } from "@gauts/auth/hono";
603
+
604
+ const core = createAuth({ db });
605
+ const hono = createHonoAdapter({
606
+ auth: core,
623
607
  });
624
608
  ```
625
609
 
626
- Access rules:
610
+ ## Next.js adapter
611
+
612
+ The Next.js adapter schedules renewal; it does not authenticate pages or API requests.
627
613
 
628
614
  ```ts
629
- const db = createPrismaAdapter({
630
- client: prisma,
631
- config: {
632
- account: {
633
- status: ["ACTIVE", "PENDING"],
634
- },
635
- user: {
636
- role: ["ADMIN"],
637
- status: ["ACTIVE", "PENDING"],
638
- },
639
- },
640
- });
615
+ import type { NextRequest } from "next/server";
616
+ import { NextResponse } from "next/server";
617
+
618
+ export const proxy = async (request: NextRequest) => {
619
+ const response = NextResponse.next();
620
+ const renewal = await nextAuth.renew({ request, response });
621
+
622
+ if (renewal.status === 401) {
623
+ return NextResponse.redirect(new URL("/auth/login", request.url));
624
+ }
625
+
626
+ if (renewal.status !== null && renewal.status >= 500) {
627
+ return NextResponse.redirect(new URL("/maintenance", request.url));
628
+ }
629
+
630
+ return renewal.response;
631
+ };
641
632
  ```
642
633
 
643
- 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.
634
+ Result values:
635
+
636
+ | `attempted` | `status` | Meaning |
637
+ | :---------: | ----------: | --------------------------------------------------------------- |
638
+ | `false` | `null` | Session token exists and renewal is not due. |
639
+ | `false` | `401` | Session token is missing or malformed; no API request occurred. |
640
+ | `true` | HTTP status | The renewal endpoint was called and returned this status. |
644
641
 
645
- The configured arrays are access allowlists, not declarations of every enum value that exists in the application.
642
+ 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.
646
643
 
647
- Custom database adapters implement:
644
+ `buildForwardHeaders()` and `FORWARD_HEADERS` are exported from `@gauts/auth/next` for application fetchers that need the same controlled forwarding rules:
648
645
 
649
646
  ```ts
650
- import type { DbAdapter } from "@gauts/auth";
647
+ import { buildForwardHeaders } from "@gauts/auth/next";
651
648
 
652
- const db = {
653
- create: async (session) => {},
654
- find: async ({ account_id, session_id }) => null,
655
- findActive: async ({ account_id, now }) => [],
656
- findToken: async (token_hash) => null,
657
- revoke: async ({ revoked_at, session_ids }) => {},
658
- updateExpiry: async ({ expires_at, session_id, updated_at }) => {},
659
- } satisfies DbAdapter;
649
+ const headers = buildForwardHeaders({
650
+ headers: incoming,
651
+ extra: ["cookie", "authorization"],
652
+ });
660
653
  ```
661
654
 
662
- `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.
655
+ Safe client and proxy metadata is copied by default. Credentials such as `cookie` and `authorization` are excluded unless explicitly listed in `extra`.
656
+
657
+ 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.
658
+
659
+ Apply renewal only to protected routes or skip public routes before calling `nextAuth.renew()`.
663
660
 
664
661
  ## Core session API
665
662
 
@@ -673,27 +670,46 @@ await auth.session.revokeToken(token);
673
670
  await auth.session.revokeAccount(account_id);
674
671
  ```
675
672
 
676
- - `resolve` performs read-only authentication.
677
- - `renew` validates and updates expiry only when due.
678
- - `list` returns active, non-expired sessions without token hashes.
679
- - revocation retains database history through `revoked_at`.
680
- - the package does not limit session count or delete historical rows; retention and cleanup belong to the application.
673
+ | Method | Behavior |
674
+ | --------------- | ------------------------------------------------------ |
675
+ | `create` | Creates a session and returns the raw token once. |
676
+ | `resolve` | Performs read-only DB authentication. |
677
+ | `renew` | Validates through DB and updates expiry only when due. |
678
+ | `list` | Returns active sessions without token hashes. |
679
+ | `revoke` | Revokes one session belonging to an account. |
680
+ | `revokeToken` | Revokes the session matching a raw token. |
681
+ | `revokeAccount` | Revokes every active session for an account. |
681
682
 
682
- ## Performance and cache policy
683
+ The package does not limit session count or delete historical rows. Retention and cleanup belong to the application.
683
684
 
684
- 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`.
685
+ ## Custom database adapter
685
686
 
686
- Without cache, each `requireSession` performs:
687
+ The core depends on `DbAdapter`, not Prisma:
687
688
 
688
- ```text
689
- 1 Prisma relation lookup by indexed account_sessions.token_hash
689
+ ```ts
690
+ import type { DbAdapter } from "@gauts/auth";
691
+
692
+ const db = {
693
+ create: async (session) => {},
694
+ find: async ({ account_id, session_id }) => null,
695
+ findActive: async ({ account_id, now }) => [],
696
+ findToken: async (token_hash) => null,
697
+ revoke: async ({ revoked_at, session_ids }) => {},
698
+ updateExpiry: async ({ expires_at, session_id, updated_at }) => {},
699
+ } satisfies DbAdapter;
690
700
  ```
691
701
 
692
- 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.
702
+ `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.
703
+
704
+ ## Performance
693
705
 
694
- 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.
706
+ The package contains no Redis or in-process cache.
695
707
 
696
- 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.
708
+ Without the optional browser cache, each `requireSession` performs an indexed database lookup through `account_sessions.token_hash`.
709
+
710
+ With a valid cache, `GET` and `HEAD` skip the lookup until `cache.ttl` expires. Unsafe methods always use current database state.
711
+
712
+ 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.
697
713
 
698
714
  ## Errors
699
715
 
@@ -710,31 +726,29 @@ type AuthErrorCode =
710
726
 
711
727
  Use `isAuthError(error)` before reading `error.code`.
712
728
 
713
- The package does not choose HTTP responses. A typical application mapping is:
714
-
715
- | Code | Suggested HTTP status |
716
- | --- | ---: |
717
- | `AUTH_CONFIG_INVALID` | `500` during startup |
718
- | `PASSWORD_INPUT_INVALID` | `400` |
719
- | `SESSION_CLIENT_MISMATCH` | `403` |
720
- | `SESSION_DATA_INVALID` | `400` |
721
- | `SESSION_INVALID` | `401` |
722
- | `SESSION_NOT_FOUND` | `404` |
723
- | `DB_UNAVAILABLE` | `503` |
729
+ | Code | Suggested HTTP status | Meaning |
730
+ | ------------------------- | --------------------: | ----------------------------------------------------------------- |
731
+ | `AUTH_CONFIG_INVALID` | `500` | Invalid startup configuration. |
732
+ | `PASSWORD_INPUT_INVALID` | `400` | Password input violates configured limits. |
733
+ | `SESSION_CLIENT_MISMATCH` | `403` | A configured client field does not match; the session is revoked. |
734
+ | `SESSION_DATA_INVALID` | `400` | Invalid session or renewal data. |
735
+ | `SESSION_INVALID` | `401` | Missing, expired, revoked, or unknown session. |
736
+ | `SESSION_NOT_FOUND` | `404` | Requested session does not exist for the account. |
737
+ | `DB_UNAVAILABLE` | `503` | Database operation failed. Authentication fails closed. |
724
738
 
725
- Applications may use a different response policy, but database failures and invalid sessions must continue to fail closed.
739
+ The package throws typed errors but does not choose application HTTP responses.
726
740
 
727
741
  ## Security responsibilities
728
742
 
729
- The package provides session primitives, not a complete application security policy. Consuming applications remain responsible for:
743
+ The package provides authentication primitives, not a complete application security policy. Applications remain responsible for:
730
744
 
731
745
  - TLS and trusted-proxy configuration;
732
- - CSRF, CORS, and origin validation;
746
+ - CSRF, CORS, host, and origin validation;
733
747
  - login and renewal rate limiting;
734
748
  - equivalent password verification work for unknown accounts;
735
- - account status and authorization rules;
736
- - re-authentication for sensitive actions;
737
- - database migrations and cleanup;
749
+ - route roles and authorization;
750
+ - re-authentication for sensitive operations;
751
+ - database migrations and session cleanup;
738
752
  - never logging passwords, raw tokens, cookie headers, or password hashes.
739
753
 
740
- Exact IP/User-Agent/platform matching is defense in depth. It does not prevent every stolen-cookie replay scenario.
754
+ Exact IP, User-Agent, and platform validation are defense in depth. They do not prevent every stolen-cookie replay scenario.