@gauts/auth 0.2.2 → 0.3.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.
Files changed (58) hide show
  1. package/README.md +465 -438
  2. package/SECURITY.md +44 -1
  3. package/dist/adapters/hono/index.d.ts +33 -19
  4. package/dist/adapters/hono/index.d.ts.map +1 -1
  5. package/dist/adapters/hono/index.js +134 -55
  6. package/dist/adapters/hono/index.js.map +1 -1
  7. package/dist/adapters/next/index.d.ts +24 -0
  8. package/dist/adapters/next/index.d.ts.map +1 -0
  9. package/dist/adapters/next/index.js +93 -0
  10. package/dist/adapters/next/index.js.map +1 -0
  11. package/dist/adapters/prisma/index.d.ts +12 -4
  12. package/dist/adapters/prisma/index.d.ts.map +1 -1
  13. package/dist/adapters/prisma/index.js +95 -9
  14. package/dist/adapters/prisma/index.js.map +1 -1
  15. package/dist/auth.d.ts +8 -6
  16. package/dist/auth.d.ts.map +1 -1
  17. package/dist/auth.js +7 -9
  18. package/dist/auth.js.map +1 -1
  19. package/dist/client/index.d.ts +5 -0
  20. package/dist/client/index.d.ts.map +1 -1
  21. package/dist/client/index.js +6 -1
  22. package/dist/client/index.js.map +1 -1
  23. package/dist/config.d.ts +2 -2
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +8 -7
  26. package/dist/config.js.map +1 -1
  27. package/dist/errors.d.ts +1 -1
  28. package/dist/errors.d.ts.map +1 -1
  29. package/dist/errors.js.map +1 -1
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/session/cache.d.ts +32 -0
  33. package/dist/session/cache.d.ts.map +1 -0
  34. package/dist/session/cache.js +136 -0
  35. package/dist/session/cache.js.map +1 -0
  36. package/dist/session/cookie.d.ts +15 -0
  37. package/dist/session/cookie.d.ts.map +1 -0
  38. package/dist/session/cookie.js +31 -0
  39. package/dist/session/cookie.js.map +1 -0
  40. package/dist/session/guards.d.ts +6 -0
  41. package/dist/session/guards.d.ts.map +1 -0
  42. package/dist/session/guards.js +23 -0
  43. package/dist/session/guards.js.map +1 -0
  44. package/dist/session/service.d.ts +2 -3
  45. package/dist/session/service.d.ts.map +1 -1
  46. package/dist/session/service.js +117 -162
  47. package/dist/session/service.js.map +1 -1
  48. package/dist/session/types.d.ts +47 -58
  49. package/dist/session/types.d.ts.map +1 -1
  50. package/package.json +9 -10
  51. package/dist/adapters/redis/index.d.ts +0 -11
  52. package/dist/adapters/redis/index.d.ts.map +0 -1
  53. package/dist/adapters/redis/index.js +0 -62
  54. package/dist/adapters/redis/index.js.map +0 -1
  55. package/dist/session/schema.d.ts +0 -5
  56. package/dist/session/schema.d.ts.map +0 -1
  57. package/dist/session/schema.js +0 -56
  58. package/dist/session/schema.js.map +0 -1
package/README.md CHANGED
@@ -1,55 +1,136 @@
1
1
  # `@gauts/auth`
2
2
 
3
- Reusable password authentication and opaque server-side sessions for Node.js applications.
3
+ Reusable password authentication and database-backed opaque sessions for Node.js applications.
4
4
 
5
- `@gauts/auth` validates sessions through Redis and stores durable session history in a database. Redis is the authentication authority; the database is never used as an authentication fallback.
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 renewal marker
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.
6
14
 
7
15
  ## Requirements
8
16
 
9
17
  - Node.js 22 or newer.
10
- - A connected Redis client.
11
- - A Prisma client containing the required session model, or a custom `DbAdapter`.
12
- - Hono 4 when using the Hono adapter.
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`.
13
21
 
14
22
  ## Installation
15
23
 
16
- For Hono, Prisma, and Redis:
24
+ For Hono and Prisma:
25
+
26
+ ```bash
27
+ npm install @gauts/auth hono @prisma/client
28
+ ```
29
+
30
+ For the Next.js adapter, the application must already use Next.js:
17
31
 
18
32
  ```bash
19
- npm install @gauts/auth hono redis @prisma/client
33
+ npm install @gauts/auth next
20
34
  ```
21
35
 
22
- `hono` and `redis` are optional peer dependencies. The Prisma adapter does not import Prisma at runtime; it receives the generated client from the application.
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. |
23
46
 
24
47
  ## Flow
25
48
 
26
49
  Login:
27
50
 
28
51
  ```text
29
- password -> configured algorithm -> database session row
30
- -> Redis session
31
- -> opaque HttpOnly cookie
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 24-hour renewal marker
59
+ -> optionally write signed short cache
32
60
  ```
33
61
 
34
- Authenticated HTTP request:
62
+ Protected `GET` or `HEAD`:
35
63
 
36
64
  ```text
37
- cookie -> SHA-256 hash -> Redis -> client validation -> route
38
- -> renewal due -> database expiry
39
- -> Redis TTL
40
- -> cookie expiry
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
41
69
  ```
42
70
 
43
- The browser receives one random 256-bit opaque token. The token remains the same throughout the session. Redis and the database store only its SHA-256 hash.
71
+ Unsafe methods and direct core calls:
44
72
 
45
- Sessions use sliding inactivity expiry. Activity before `renewInterval` performs no expiry write. The first eligible HTTP request after `renewInterval` extends the database expiry, Redis TTL, and browser cookie expiry.
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:
46
83
 
47
- ## Prisma model
84
+ ```text
85
+ Next checks the renewal marker
86
+ -> marker exists: no API call
87
+ -> marker missing: 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 marker, and a fresh cache
91
+ ```
48
92
 
49
- The default Prisma model is `auth_sessions`:
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.
50
108
 
51
109
  ```prisma
52
- model auth_sessions {
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 {
53
134
  id String @id @default(uuid()) @db.VarChar(255)
54
135
  account_id String @db.VarChar(255)
55
136
  token_hash String @unique @db.VarChar(64)
@@ -61,130 +142,202 @@ model auth_sessions {
61
142
  created_at DateTime @default(now()) @db.Timestamp(0)
62
143
  updated_at DateTime? @db.Timestamp(0)
63
144
 
145
+ account user_accounts @relation(fields: [account_id], references: [id], onDelete: Cascade)
146
+
64
147
  @@index([account_id])
65
148
  @@index([expires_at])
66
149
  @@index([revoked_at])
67
150
  }
68
151
  ```
69
152
 
70
- These field names and compatible types are required. Additional indexes, relations, and optional/defaulted fields are allowed. Do not add required fields without defaults unless the application supplies them separately.
153
+ The adapter requires these Prisma field and relation names:
71
154
 
72
- Create migrations through the consuming application's normal Prisma workflow. The package never creates or runs migrations.
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` |
73
160
 
74
- ## Quick start with Hono
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.
75
162
 
76
- ### 1. Define the session data
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.
77
164
 
78
- The generic passed to `createHonoAuth` defines the application data cached in Redis and exposed on authenticated requests. Keep it small and never include secrets or password hashes.
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.
79
166
 
80
- ```ts
81
- type AccountSession = {
82
- email: string;
83
- role: "admin" | "owner";
84
- };
85
- ```
167
+ Create migrations through the consuming application's normal Prisma workflow. The package never creates or runs migrations.
86
168
 
87
- ### 2. Create one auth instance
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
88
174
 
89
175
  ```ts
90
176
  import { createHonoAuth } from "@gauts/auth/hono";
91
- import { createDbAdapter } from "@gauts/auth/prisma";
92
- import { createRedisAdapter } from "@gauts/auth/redis";
177
+ import { createPrismaAdapter } from "@gauts/auth/prisma";
93
178
 
94
- export const auth = createHonoAuth<AccountSession>({
95
- getIp: (c) => getTrustedClientIp(c),
96
- db: createDbAdapter({
179
+ export const auth = createHonoAuth({
180
+ secret: process.env.AUTH_SECRET,
181
+
182
+ db: createPrismaAdapter({
97
183
  client: prisma,
184
+ config: {
185
+ account: {
186
+ status: ["ACTIVE", "PENDING"],
187
+ },
188
+ user: {
189
+ status: ["ACTIVE", "PENDING"],
190
+ },
191
+ },
98
192
  }),
99
- redis: createRedisAdapter({
100
- client: redis,
101
- config: { prefix: "my-app:auth" },
102
- }),
193
+
194
+ getIp: (c) => getTrustedClientIp(c),
195
+
196
+ session: {
197
+ validation: ["agent"],
198
+ },
199
+
200
+ cache: {
201
+ ttl: 60,
202
+ },
103
203
  });
104
204
  ```
105
205
 
106
- Both clients must already be initialized by the application. The package does not connect, reconnect, disconnect, or close them.
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.
107
207
 
108
- `createDbAdapter` uses `auth_sessions` by default. `config` is optional:
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:
109
209
 
110
210
  ```ts
111
- const db = createDbAdapter({
112
- client: prisma,
113
- config: {
114
- table: "admin_sessions",
115
- },
211
+ import { createAuth } from "@gauts/auth";
212
+ import { createHonoAdapter } from "@gauts/auth/hono";
213
+
214
+ const core = createAuth({ db });
215
+ const hono = createHonoAdapter({
216
+ auth: core,
217
+ cache: { ttl: 60 },
218
+ getIp: (c) => getTrustedClientIp(c),
219
+ secret: process.env.AUTH_SECRET,
116
220
  });
117
221
  ```
118
222
 
119
- When `table` is supplied, TypeScript only accepts a compatible model from that generated Prisma client.
223
+ Do not create a second core for the adapter; pass the existing `core` instance through `auth`.
224
+
225
+ 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.
120
226
 
121
- ### 3. Type the Hono application
227
+ ### Type the application
122
228
 
123
229
  ```ts
124
230
  import { Hono } from "hono";
125
231
  import type { HonoAuthEnv } from "@gauts/auth/hono";
126
232
 
127
- const app = new Hono<HonoAuthEnv<AccountSession>>();
233
+ const app = new Hono<HonoAuthEnv>();
128
234
  ```
129
235
 
130
- `auth.requireSession` installs both values:
236
+ `auth.requireSession` installs:
131
237
 
132
238
  ```ts
133
239
  const session = c.get("session");
134
240
  const account = c.get("account");
241
+ const user = c.get("user");
135
242
  ```
136
243
 
137
- ### 4. Login
244
+ The values have these shapes:
138
245
 
139
- The application owns request validation, account lookup, rate limiting, status checks, and error responses.
246
+ ```ts
247
+ type AuthAccount = {
248
+ email: string;
249
+ id: string;
250
+ name: string;
251
+ role: string;
252
+ status: string;
253
+ timezone: string | null;
254
+ user: AuthUser;
255
+ };
256
+
257
+ type AuthUser = {
258
+ id: string;
259
+ role: string;
260
+ status: string;
261
+ };
262
+
263
+ type Session = {
264
+ account_id: string;
265
+ client: {
266
+ agent: string | null;
267
+ ip: string | null;
268
+ platform: string | null;
269
+ };
270
+ created_at: Date;
271
+ expires_at: Date;
272
+ id: string;
273
+ renew_at: Date;
274
+ };
275
+ ```
276
+
277
+ ### Login
278
+
279
+ 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.
140
280
 
141
281
  ```ts
282
+ const DUMMY_PASSWORD_HASH =
283
+ "$argon2id$v=19$m=65536,p=4,t=3$PUotpfVXonc0VRFuV1pKZQ$oxxA8DMvGRTSbZvh2Dkokeyih9sbKeodWYROqVxP9BI";
284
+
142
285
  app.post("/auth/login", async (c) => {
143
- const { email, password } = await c.req.json<{
286
+ const body = await c.req.json<{
144
287
  email: string;
145
288
  password: string;
146
289
  }>();
147
- const account = await findAccount(email);
148
-
149
- if (
150
- !account ||
151
- !(await auth.password.verify({
152
- password,
153
- storedHash: account.password_hash,
154
- }))
155
- ) {
290
+ const account = await findAccount(body.email);
291
+ const passwordValid = await auth.password.verify({
292
+ password: body.password,
293
+ storedHash: account?.passwordHash ?? DUMMY_PASSWORD_HASH,
294
+ });
295
+
296
+ if (!account || !passwordValid) {
156
297
  return c.json({ error: "Invalid credentials." }, 401);
157
298
  }
158
299
 
159
- const session = await auth.createSession({
300
+ await auth.createSession({
160
301
  account_id: account.id,
161
302
  context: c,
162
- data: {
163
- email: account.email,
164
- role: account.role,
165
- },
166
303
  });
167
304
 
168
- return c.json({ account: session.data });
305
+ return c.json({ authenticated: true });
169
306
  });
170
307
  ```
171
308
 
172
- `createSession` inserts the database row, creates the Redis session, writes the cookie, and returns the public session. The raw token remains internal to the Hono adapter.
309
+ 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.
173
310
 
174
- ### 5. Protect routes
311
+ Only `account_id` is persisted by the session. Email, roles, statuses, password hashes, and other dynamic account data never enter the session table.
312
+
313
+ ### Protect routes
175
314
 
176
315
  ```ts
177
316
  app.get("/account", auth.requireSession, (c) => {
178
317
  return c.json({
179
318
  account: c.get("account"),
180
319
  session_id: c.get("session").id,
320
+ user: c.get("user"),
181
321
  });
182
322
  });
183
323
  ```
184
324
 
185
- `requireSession` authenticates the request. It does not apply application roles or permissions.
325
+ `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.
326
+
327
+ ### Renewal endpoint
328
+
329
+ ```ts
330
+ app.post("/auth/renew", async (c) => {
331
+ await auth.renewSession(c);
332
+ return c.body(null, 204);
333
+ });
334
+ ```
335
+
336
+ `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.
337
+
338
+ If renewal is not yet due, database expiry is not changed. The API still returns the authoritative session cookie, renewal marker, and cache.
186
339
 
187
- ### 6. Logout
340
+ ### Logout
188
341
 
189
342
  ```ts
190
343
  app.post("/auth/logout", async (c) => {
@@ -193,33 +346,66 @@ app.post("/auth/logout", async (c) => {
193
346
  });
194
347
  ```
195
348
 
196
- Logout deletes the Redis session, records revocation in the database, and clears the cookie. Backend revocation removes access; clearing the cookie is client cleanup.
349
+ 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.
197
350
 
198
- ## Configuration
351
+ ## Next.js renewal adapter
199
352
 
200
- ### `createHonoAuth`
353
+ The Next.js adapter does not authenticate sessions. It calls the private API renewal URL only when the renewal marker is missing.
201
354
 
202
355
  ```ts
203
- const auth = createHonoAuth<AccountSession>({
204
- getIp,
205
- db,
206
- redis,
207
- password,
208
- session,
209
- cookie,
356
+ import { createNextAuth } from "@gauts/auth/next";
357
+
358
+ export const nextAuth = createNextAuth({
359
+ renewUrl: `${process.env.NEXT_PRIVATE_API_URL}/auth/renew`,
210
360
  });
211
361
  ```
212
362
 
213
- | Property | Required | Purpose |
214
- | ---------- | -------- | -------------------------------------------------------------------- |
215
- | `getIp` | Yes | Returns the trusted client IP from the Hono context. |
216
- | `db` | Yes | Stores durable session history. |
217
- | `redis` | Yes | Stores and validates active sessions. |
218
- | `password` | No | Selects password algorithm and cost limits. Defaults to Argon2id. |
219
- | `session` | No | Configures expiry, renewal, maximum sessions, and client validation. |
220
- | `cookie` | No | Configures the Hono session cookie. |
363
+ The controlled header list is also exported for application fetchers that need to follow the same forwarding policy:
364
+
365
+ ```ts
366
+ import { FORWARD_HEADERS } from "@gauts/auth/next";
367
+ ```
368
+
369
+ Use it in `proxy.ts` before returning the browser-facing response:
370
+
371
+ ```ts
372
+ import type { NextRequest } from "next/server";
373
+ import { NextResponse } from "next/server";
374
+
375
+ export const proxy = async (request: NextRequest) => {
376
+ const response = NextResponse.next();
377
+ const renewal = await nextAuth.renew({
378
+ request,
379
+ response,
380
+ });
381
+
382
+ if (renewal.status === 401) {
383
+ return NextResponse.redirect(new URL("/login", request.url));
384
+ }
385
+
386
+ if (renewal.status !== null && renewal.status >= 500) {
387
+ return NextResponse.redirect(new URL("/maintenance", request.url));
388
+ }
389
+
390
+ return renewal.response;
391
+ };
392
+ ```
393
+
394
+ Result semantics:
395
+
396
+ | `attempted` | `status` | Meaning |
397
+ | --- | ---: | --- |
398
+ | `false` | `null` | Session token and renewal marker exist. |
399
+ | `false` | `401` | Session token is missing or malformed. No API call occurred. |
400
+ | `true` | HTTP status | `/auth/renew` was called and its `Set-Cookie` headers were copied. |
221
401
 
222
- Configuration is resolved once when the auth instance is created. Invalid configuration throws `AUTH_CONFIG_INVALID` during startup.
402
+ The adapter forwards only the session cookie and the client/origin headers required for session and CSRF validation. If the browser request has no `Origin`, it uses the public origin from `NextRequest`. 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.
403
+
404
+ Protected API endpoints remain responsible for real authentication. The renewal marker is only a browser scheduling mechanism and never acts as a refresh token.
405
+
406
+ 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.
407
+
408
+ ## Configuration
223
409
 
224
410
  ### Password
225
411
 
@@ -227,158 +413,116 @@ Argon2id is the default:
227
413
 
228
414
  ```ts
229
415
  password: {
230
- algorithm: "argon2id",
416
+ algorithm: "argon2id",
231
417
  }
232
418
  ```
233
419
 
234
- | Argon2id property | Default | Allowed | Purpose |
235
- | ----------------- | -----------: | ---------------------: | --------------------------------- |
236
- | `algorithm` | `"argon2id"` | `"argon2id"` | Selects Argon2id. May be omitted. |
237
- | `hashLength` | `32` | `16` to `64` | Hash output length in bytes. |
238
- | `maxBytes` | `1024` | `1` to `1,048,576` | Maximum UTF-8 password size. |
239
- | `memoryCost` | `65,536` | `8,192` to `1,048,576` | Memory cost in KiB. |
240
- | `parallelism` | `4` | `1` to `16` | Number of lanes. |
241
- | `timeCost` | `3` | `1` to `10` | Number of iterations. |
420
+ | Argon2id property | Default | Allowed |
421
+ | --- | ---: | ---: |
422
+ | `hashLength` | `32` | `16` to `64` |
423
+ | `maxBytes` | `1024` | `1` to `1,048,576` |
424
+ | `memoryCost` | `65,536` | `8,192` to `1,048,576` |
425
+ | `parallelism` | `4` | `1` to `16` |
426
+ | `timeCost` | `3` | `1` to `10` |
242
427
 
243
- Applications with bcrypt hashes must select bcrypt explicitly:
428
+ Applications with existing bcrypt hashes must select bcrypt explicitly:
244
429
 
245
430
  ```ts
246
431
  password: {
247
- algorithm: "bcrypt",
432
+ algorithm: "bcrypt",
248
433
  }
249
434
  ```
250
435
 
251
- | bcrypt property | Default | Allowed | Purpose |
252
- | ---------------- | -------: | ------------------------: | --------------------------------------------- |
253
- | `algorithm` | Required | `"bcrypt"` | Selects bcrypt for hashing and verification. |
254
- | `maxBytes` | `72` | `1` to `72` | Maximum UTF-8 size accepted for new hashes. |
255
- | `rounds` | `12` | `4` to `31` | Cost factor. |
256
- | `verifyMaxBytes` | `72` | `maxBytes` to `1,048,576` | Maximum UTF-8 size accepted for verification. |
436
+ | bcrypt property | Default | Allowed |
437
+ | --- | ---: | ---: |
438
+ | `maxBytes` | `72` | `1` to `72` |
439
+ | `rounds` | `12` | `4` to `31` |
440
+ | `verifyMaxBytes` | `72` | `maxBytes` to `1,048,576` |
257
441
 
258
442
  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.
259
443
 
260
- bcrypt ignores bytes after the first 72. Keep `verifyMaxBytes` at `72` unless preserving historical truncation is an explicit application requirement.
261
-
262
444
  ### Session
263
445
 
264
446
  ```ts
265
447
  session: {
266
- max: 10,
267
- renewInterval: 24 * 60 * 60,
268
- ttl: 7 * 24 * 60 * 60,
269
- validation: ["agent"],
448
+ maxLifetime: 30 * 24 * 60 * 60,
449
+ renewInterval: 24 * 60 * 60,
450
+ ttl: 7 * 24 * 60 * 60,
451
+ validation: ["agent"],
270
452
  }
271
453
  ```
272
454
 
273
455
  Time values are seconds.
274
456
 
275
- | Property | Default | Allowed | Purpose |
276
- | --------------- | ----------: | --------------------------------------------------: | ------------------------------------------------------------------------------------------ |
277
- | `max` | `10` | `1` to `10,000` | Maximum active sessions per account. |
278
- | `renewInterval` | `86,400` | `1` to `ttl - 1` | Minimum activity interval between expiry writes. |
279
- | `ttl` | `604,800` | `60` to `31,536,000` | Inactivity lifetime on creation and renewal. |
280
- | `validation` | `["agent"]` | Unique combination of `agent`, `ip`, and `platform` | Client fields compared during validation. An empty array disables client-field comparison. |
457
+ | Property | Default | Allowed | Purpose |
458
+ | --- | ---: | ---: | --- |
459
+ | `maxLifetime` | `2,592,000` | `ttl` to `31,536,000` | Maximum lifetime from the original login, regardless of activity. |
460
+ | `renewInterval` | `86,400` | `1` to `ttl - 1` | Minimum interval before renewal is due. |
461
+ | `ttl` | `604,800` | `60` to `31,536,000` | Sliding inactivity lifetime. |
462
+ | `validation` | `["agent"]` | Unique `agent`, `ip`, `platform` fields | Exact client fields compared on every validation. |
463
+
464
+ The authoritative limits are derived from the immutable creation time and the last successful renewal:
465
+
466
+ ```text
467
+ maxExpiresAt = created_at + maxLifetime
468
+ expires_at = min(now + ttl, maxExpiresAt)
469
+ renew_at = min((updated_at ?? created_at) + renewInterval, maxExpiresAt)
470
+ ```
281
471
 
282
- Expiry is sliding and has no forced absolute lifetime. The opaque token is not rotated during renewal.
472
+ `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.
283
473
 
284
474
  ### Cookie
285
475
 
286
476
  ```ts
287
477
  cookie: {
288
- domain: undefined,
289
- name: "__Host-session",
290
- path: "/",
291
- sameSite: "Lax",
292
- secure: true,
478
+ cacheName: "__cac",
479
+ domain: undefined,
480
+ name: "__sec",
481
+ path: "/",
482
+ renewName: "__ren",
483
+ sameSite: "Lax",
484
+ secure: true,
293
485
  }
294
486
  ```
295
487
 
296
- | Property | Default | Purpose |
297
- | ---------- | ------------------ | ---------------------------------------------------- |
298
- | `name` | `"__Host-session"` | Cookie name. |
299
- | `domain` | Not set | Optional domain; without it the cookie is host-only. |
300
- | `path` | `"/"` | Cookie path. |
301
- | `sameSite` | `"Lax"` | `"Strict"`, `"Lax"`, or `"None"`. |
302
- | `secure` | `true` | Sends the cookie only over HTTPS. |
303
-
304
488
  `HttpOnly` is always enabled.
305
489
 
490
+ - The session cookie contains only the stable opaque token and expires with the database session.
491
+ - The cache cookie contains the signed snapshot and expires after `cache.ttl`.
492
+ - The renewal cookie contains the fixed value `1` and expires at the authoritative `renew_at` time.
493
+ - Cookie names default to `__sec`, `__cac`, and `__ren`.
494
+ - All three names must be valid and unique.
495
+
306
496
  - `__Host-` requires `secure: true`, `path: "/"`, and no domain.
307
497
  - `__Secure-` requires `secure: true`.
308
498
  - `SameSite=None` requires `secure: true`.
309
- - Local HTTP development with `secure: false` requires a custom name without a secure prefix.
310
-
311
- ### Redis adapter
312
-
313
- ```ts
314
- import { createClient } from "redis";
315
- import { createRedisAdapter } from "@gauts/auth/redis";
316
-
317
- const redis = createClient({ url: process.env.REDIS_URL });
318
- await redis.connect();
319
-
320
- const adapter = createRedisAdapter({
321
- client: redis,
322
- config: {
323
- prefix: "my-app:auth",
324
- },
325
- });
326
- ```
327
-
328
- `config` is optional. The prefix defaults to `gauts:auth`, supports letters, numbers, `:`, `_`, and `-`, and has a maximum of 128 characters.
329
-
330
- ```text
331
- <prefix>:session:<sha256-token-hash>
332
- ```
333
-
334
- Redis failures throw `REDIS_UNAVAILABLE`. Authentication never falls back to the database.
335
-
336
- ### Prisma adapter
337
-
338
- Default model:
499
+ - Local HTTP development requires `secure: false`.
339
500
 
340
- ```ts
341
- import { createDbAdapter } from "@gauts/auth/prisma";
342
-
343
- const db = createDbAdapter({
344
- client: prisma,
345
- });
346
- ```
501
+ ### Signed session cache
347
502
 
348
- Custom compatible model:
503
+ The cache is disabled by default. Enable it explicitly:
349
504
 
350
505
  ```ts
351
- const db = createDbAdapter({
352
- client: prisma,
353
- config: {
354
- table: "admin_sessions",
355
- },
356
- });
506
+ secret: process.env.AUTH_SECRET,
507
+ cache: {
508
+ ttl: 60,
509
+ }
357
510
  ```
358
511
 
359
- The adapter owns the five database operations required by the session service: create, find one, find active, revoke, and update expiry. Applications do not implement those queries.
360
-
361
- The default model is `auth_sessions`. If that model does not exist, `config.table` is required. TypeScript rejects a selected model whose generated result does not contain the complete session record shape.
512
+ `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.
362
513
 
363
- Database failures are exposed by the auth core as `DB_UNAVAILABLE`.
514
+ The cache payload:
364
515
 
365
- ### Custom database adapter
516
+ - is signed with HMAC-SHA-256 using a domain-separated context;
517
+ - is cryptographically bound to the opaque session token;
518
+ - contains the resolved session, account, user, and its own expiry;
519
+ - compares the same configured IP, User-Agent, and platform fields on every hit;
520
+ - never extends the authoritative session expiry;
521
+ - is accepted only for `GET` and `HEAD` requests.
366
522
 
367
- Applications not using Prisma can implement the exported `DbAdapter` contract:
523
+ 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.
368
524
 
369
- ```ts
370
- import type { DbAdapter } from "@gauts/auth";
371
-
372
- const db = {
373
- create: async (session) => {},
374
- find: async ({ account_id, session_id }) => null,
375
- findActive: async ({ account_id, now }) => [],
376
- revoke: async ({ revoked_at, session_ids }) => {},
377
- updateExpiry: async ({ expires_at, session_id, updated_at }) => {},
378
- } satisfies DbAdapter;
379
- ```
380
-
381
- The adapter must persist only token hashes, never raw tokens. `find` must scope by both `account_id` and `session_id`. `findActive` must return only non-revoked rows whose `expires_at` is greater than `now`.
525
+ 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.
382
526
 
383
527
  ### Trusted client IP
384
528
 
@@ -386,11 +530,9 @@ The adapter must persist only token hashes, never raw tokens. `find` must scope
386
530
  getIp: (c) => getTrustedClientIp(c);
387
531
  ```
388
532
 
389
- `getIp` runs during login and every authenticated HTTP request. It may be synchronous or asynchronous and returns `string`, `null`, or `undefined`.
533
+ 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.
390
534
 
391
- Only the application knows which reverse proxies and forwarding headers are trusted. The package does not select `X-Forwarded-For`, `CF-Connecting-IP`, socket addresses, or another source. It canonicalizes the value returned by the application.
392
-
393
- The Hono adapter reads the remaining metadata from request headers:
535
+ The Hono adapter reads:
394
536
 
395
537
  ```ts
396
538
  type SessionClientInput = {
@@ -401,278 +543,163 @@ type SessionClientInput = {
401
543
  ```
402
544
 
403
545
  - IPv4, IPv4-mapped IPv6, and IPv6 are canonicalized.
404
- - `::1` becomes `127.0.0.1`.
405
546
  - Invalid or empty IP values become `null`.
406
- - Platform comes from `Sec-CH-UA-Platform`, is unquoted and limited to 255 characters.
407
- - Agent comes from `User-Agent` and is stored in full without truncation.
408
- - No GeoIP, DNS, country, or database lookup is performed.
409
-
410
- ## Password API
547
+ - Platform comes from `Sec-CH-UA-Platform`, is normalized, and is limited to 255 characters.
548
+ - User-Agent comes from `User-Agent` and is stored in full.
549
+ - No GeoIP, DNS, country, or external lookup is performed.
550
+ - Every field selected in `session.validation` is required during creation and validation. Missing or invalid configured fields never match.
411
551
 
412
- ### `auth.password.algorithm`
552
+ ## Database adapter
413
553
 
414
- The resolved algorithm: `"argon2id"` or `"bcrypt"`.
554
+ 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.
415
555
 
416
- ### `auth.password.hash(password)`
417
-
418
- Validates the UTF-8 byte length and creates a hash using the configured algorithm.
556
+ Default Prisma delegate (`account_sessions`):
419
557
 
420
558
  ```ts
421
- const password_hash = await auth.password.hash(password);
422
- ```
559
+ import { createPrismaAdapter } from "@gauts/auth/prisma";
423
560
 
424
- ### `auth.password.verify({ password, storedHash })`
425
-
426
- Verifies only with the configured algorithm. A hash from another algorithm or an invalid hash returns `false`.
427
-
428
- ```ts
429
- const valid = await auth.password.verify({
430
- password,
431
- storedHash: account.password_hash,
561
+ const db = createPrismaAdapter({
562
+ client: prisma,
563
+ config: {
564
+ account: {
565
+ status: ["ACTIVE"],
566
+ },
567
+ user: {
568
+ status: ["ACTIVE"],
569
+ },
570
+ },
432
571
  });
433
572
  ```
434
573
 
435
- Invalid password size throws `PASSWORD_INPUT_INVALID`. Wrong credentials return `false`.
436
-
437
- ## Hono API
438
-
439
- ### `auth.createSession({ account_id, context, data })`
440
-
441
- Creates the database and Redis session, writes the HttpOnly cookie, and returns `Session<TData>`.
442
-
443
- ### `auth.resolveSession(context)`
444
-
445
- Reads and validates the cookie and returns `Session<TData>`.
446
-
447
- - Throws `SESSION_INVALID` when the cookie or Redis session is missing or invalid.
448
- - Clears an invalid cookie.
449
- - Compares configured client fields.
450
- - Renews database expiry, Redis TTL, and cookie expiry only when due.
451
- - Revokes the backend session and throws `SESSION_CLIENT_MISMATCH` after a mismatch.
452
-
453
- ### `auth.requireSession`
454
-
455
- Middleware that calls `resolveSession` and sets:
574
+ Custom compatible Prisma delegate:
456
575
 
457
576
  ```ts
458
- c.set("session", session);
459
- c.set("account", session.data);
577
+ const db = createPrismaAdapter({
578
+ client: prisma,
579
+ config: {
580
+ account: {
581
+ status: ["ACTIVE"],
582
+ },
583
+ table: "admin_sessions",
584
+ user: {
585
+ status: ["ACTIVE"],
586
+ },
587
+ },
588
+ });
460
589
  ```
461
590
 
462
- ### `auth.revokeSession(context)`
463
-
464
- Revokes the session represented by the request cookie and clears that cookie. Returns the revoked session IDs.
465
-
466
- ### `auth.clearSession(context)`
467
-
468
- Deletes only the response cookie. It does not revoke Redis or update the database and must not be used as logout.
469
-
470
- ### `auth.getToken(context)`
471
-
472
- Returns the raw cookie token or `null`. Never log, persist, or expose this value in a response.
473
-
474
- ## Core session API
475
-
476
- The same instance exposes the framework-independent `auth.session` service.
477
-
478
- ### `auth.session.create({ account_id, client, data })`
479
-
480
- Creates a session and returns:
591
+ Access rules:
481
592
 
482
593
  ```ts
483
- {
484
- session: Session<TData>;
485
- token: string;
486
- }
594
+ const db = createPrismaAdapter({
595
+ client: prisma,
596
+ config: {
597
+ account: {
598
+ status: ["ACTIVE", "PENDING"],
599
+ },
600
+ user: {
601
+ role: ["ADMIN"],
602
+ status: ["ACTIVE", "PENDING"],
603
+ },
604
+ },
605
+ });
487
606
  ```
488
607
 
489
- Prefer `auth.createSession` in Hono applications because the core cannot write the browser cookie.
608
+ 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.
490
609
 
491
- ### `auth.session.resolve({ client, token })`
610
+ The configured arrays are access allowlists, not declarations of every enum value that exists in the application.
492
611
 
493
- Validates and renews when due. It returns `null` for an invalid or expired session, otherwise:
612
+ Custom database adapters implement:
494
613
 
495
614
  ```ts
496
- {
497
- renewed: boolean;
498
- session: Session<TData>;
499
- }
500
- ```
501
-
502
- A framework using the core directly must update the cookie expiry when `renewed` is `true`.
503
-
504
- ### `auth.session.validate({ client, token })`
505
-
506
- Validates without renewing Redis, database expiry, or cookie expiry. A configured client mismatch still revokes the backend session.
507
-
508
- ### `auth.session.list(account_id)`
509
-
510
- Returns sessions that are active in both the database and Redis. The public result never contains `token_hash`.
511
-
512
- ### `auth.session.revoke({ account_id, session_id })`
513
-
514
- Revokes one session after confirming it belongs to the supplied account. Throws `SESSION_NOT_FOUND` when it is absent or already revoked.
515
-
516
- ### `auth.session.revokeAccount(account_id)`
517
-
518
- Revokes every active session for one account.
519
-
520
- ### `auth.session.revokeToken(token)`
521
-
522
- Revokes the Redis session represented by a raw token and records the revocation in the database. Invalid or absent tokens return an empty array.
523
-
524
- ### `auth.session.sync({ account_id, data })`
525
-
526
- Replaces the cached `data` in every active Redis session for the account while preserving tokens and TTLs.
527
-
528
- Use it after changing cached account data. Use `revokeAccount` when a change must force re-authentication.
529
-
530
- ## Public session shapes
531
-
532
- ```ts
533
- type Session<TData> = {
534
- account_id: string;
535
- client: {
536
- agent: string | null;
537
- ip: string | null;
538
- platform: string | null;
539
- };
540
- created_at: Date;
541
- data: TData;
542
- expires_at: Date;
543
- id: string;
544
- touched_at: Date;
545
- };
546
- ```
615
+ import type { DbAdapter } from "@gauts/auth";
547
616
 
548
- ```ts
549
- type ActiveSession = {
550
- account_id: string;
551
- agent: string | null;
552
- created_at: Date;
553
- expires_at: Date;
554
- id: string;
555
- ip: string | null;
556
- platform: string | null;
557
- revoked_at: Date | null;
558
- updated_at: Date | null;
559
- };
617
+ const db = {
618
+ create: async (session) => {},
619
+ find: async ({ account_id, session_id }) => null,
620
+ findActive: async ({ account_id, now }) => [],
621
+ findToken: async (token_hash) => null,
622
+ revoke: async ({ revoked_at, session_ids }) => {},
623
+ updateExpiry: async ({ expires_at, session_id, updated_at }) => {},
624
+ } satisfies DbAdapter;
560
625
  ```
561
626
 
562
- ## Client validation
627
+ `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.
563
628
 
564
- The default compares the complete User-Agent:
629
+ ## Core session API
565
630
 
566
631
  ```ts
567
- session: {
568
- validation: ["agent"],
569
- }
632
+ await auth.session.create({ account_id, client });
633
+ await auth.session.resolve({ client, token });
634
+ await auth.session.renew({ client, token });
635
+ await auth.session.list(account_id);
636
+ await auth.session.revoke({ account_id, session_id });
637
+ await auth.session.revokeToken(token);
638
+ await auth.session.revokeAccount(account_id);
570
639
  ```
571
640
 
572
- An administration application can also bind IP and platform:
641
+ - `resolve` performs read-only authentication.
642
+ - `renew` validates and updates expiry only when due.
643
+ - `list` returns active, non-expired sessions without token hashes.
644
+ - revocation retains database history through `revoked_at`.
645
+ - the package does not limit session count or delete historical rows; retention and cleanup belong to the application.
573
646
 
574
- ```ts
575
- session: {
576
- validation: ["ip", "agent", "platform"],
577
- }
578
- ```
647
+ ## Performance and cache policy
579
648
 
580
- An empty array validates only the opaque token:
649
+ 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`.
581
650
 
582
- ```ts
583
- session: {
584
- validation: [],
585
- }
586
- ```
651
+ Without cache, each `requireSession` performs:
587
652
 
588
- Comparison is exact after normalization. A configured mismatch deletes the Redis session and records revocation in the database, so both the suspicious client and legitimate browser must authenticate again.
653
+ ```text
654
+ 1 Prisma relation lookup by indexed account_sessions.token_hash
655
+ ```
589
656
 
590
- Exact IP validation can log out legitimate users on VPN, mobile, or rotating networks. Client matching is defense in depth; it does not replace TLS, secure cookies, CSRF protection, XSS prevention, or explicit re-authentication for sensitive actions.
657
+ 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.
591
658
 
592
- ## Expiration and renewal
659
+ 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.
593
660
 
594
- - Creation assigns `ttl` to Redis, the database row, and the cookie.
595
- - Requests before `renewInterval` validate without expiry writes.
596
- - The first eligible HTTP request after `renewInterval` sets expiry to `now + ttl` in Redis and the database.
597
- - The Hono adapter sends `Set-Cookie` only when renewal occurs.
598
- - The opaque token never changes during renewal.
599
- - Continued eligible HTTP activity can keep a session alive indefinitely.
600
- - A session without renewal activity expires after `ttl`.
601
- - `validate` never renews; `resolve` renews only when due.
661
+ 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.
602
662
 
603
663
  ## Errors
604
664
 
605
- All package errors have `name: "AuthError"` and a typed `code`.
606
-
607
- ```ts
608
- import { isAuthError } from "@gauts/auth";
609
-
610
- app.onError((error, c) => {
611
- if (!isAuthError(error)) {
612
- return c.json({ error: "Internal server error." }, 500);
613
- }
614
-
615
- if (error.code === "REDIS_UNAVAILABLE" || error.code === "DB_UNAVAILABLE") {
616
- return c.json({ error: "Authentication service unavailable." }, 503);
617
- }
618
-
619
- return c.json({ error: error.message }, 401);
620
- });
621
- ```
622
-
623
- | Code | Meaning |
624
- | ------------------------- | ---------------------------------------------------------------------- |
625
- | `AUTH_CONFIG_INVALID` | Invalid startup configuration or adapter contract. |
626
- | `PASSWORD_INPUT_INVALID` | Empty or oversized password input. |
627
- | `SESSION_CLIENT_MISMATCH` | A configured client field changed and the backend session was revoked. |
628
- | `SESSION_DATA_INVALID` | Required session input or stored Redis payload is invalid. |
629
- | `SESSION_INVALID` | Hono cookie or Redis session is missing, expired, or invalid. |
630
- | `SESSION_LIMIT_REACHED` | The account already has the maximum active sessions. |
631
- | `SESSION_NOT_FOUND` | The selected session is absent or already revoked. |
632
- | `DB_UNAVAILABLE` | The database adapter failed or returned invalid session data. |
633
- | `REDIS_UNAVAILABLE` | Redis failed or returned invalid data; authentication fails closed. |
634
-
635
- Applications decide final HTTP statuses and public messages.
636
-
637
- ## Framework-independent usage
638
-
639
665
  ```ts
640
- import { createAuth } from "@gauts/auth";
641
- import { createDbAdapter } from "@gauts/auth/prisma";
642
- import { createRedisAdapter } from "@gauts/auth/redis";
643
-
644
- const auth = createAuth<AccountSession>({
645
- db: createDbAdapter({ client: prisma }),
646
- redis: createRedisAdapter({ client: redis }),
647
- });
648
- ```
649
-
650
- The application must then extract client input, read and secure the raw token, call `resolve`, and deliver renewed cookie expiry when `renewed` is `true`.
651
-
652
- Hono applications should normally use `createHonoAuth`. `createHonoAdapter` remains available when deliberately composing the core and Hono adapter separately.
653
-
654
- ## Application responsibilities
655
-
656
- The package does not provide:
657
-
658
- - Registration, account lookup, OTP, OAuth, password reset, or email flows.
659
- - Endpoints or UI components.
660
- - Roles, permissions, or application authorization policies.
661
- - Prisma migrations or database/Redis connection lifecycle.
662
- - Trusted proxy policy.
663
- - Rate limiting, CSRF, CORS, CSP, logging, or notifications.
664
-
665
- The application must use HTTPS in production, protect login endpoints, validate input, prevent account enumeration, define trusted proxies, and apply CSRF and XSS protections.
666
-
667
- Never log raw passwords, raw session tokens, cookies, password hashes, or Redis session payloads.
668
-
669
- ## Package exports
670
-
671
- ```text
672
- @gauts/auth createAuth, isAuthError, and core types
673
- @gauts/auth/prisma createDbAdapter and Prisma adapter types
674
- @gauts/auth/redis createRedisAdapter and Redis adapter types
675
- @gauts/auth/hono createHonoAuth, createHonoAdapter, and Hono types
676
- ```
677
-
678
- The compiled Hono example is available in [`examples/hono`](./examples/hono).
666
+ type AuthErrorCode =
667
+ | "AUTH_CONFIG_INVALID"
668
+ | "PASSWORD_INPUT_INVALID"
669
+ | "SESSION_CLIENT_MISMATCH"
670
+ | "SESSION_DATA_INVALID"
671
+ | "SESSION_INVALID"
672
+ | "SESSION_NOT_FOUND"
673
+ | "DB_UNAVAILABLE";
674
+ ```
675
+
676
+ Use `isAuthError(error)` before reading `error.code`.
677
+
678
+ The package does not choose HTTP responses. A typical application mapping is:
679
+
680
+ | Code | Suggested HTTP status |
681
+ | --- | ---: |
682
+ | `AUTH_CONFIG_INVALID` | `500` during startup |
683
+ | `PASSWORD_INPUT_INVALID` | `400` |
684
+ | `SESSION_CLIENT_MISMATCH` | `403` |
685
+ | `SESSION_DATA_INVALID` | `400` |
686
+ | `SESSION_INVALID` | `401` |
687
+ | `SESSION_NOT_FOUND` | `404` |
688
+ | `DB_UNAVAILABLE` | `503` |
689
+
690
+ Applications may use a different response policy, but database failures and invalid sessions must continue to fail closed.
691
+
692
+ ## Security responsibilities
693
+
694
+ The package provides session primitives, not a complete application security policy. Consuming applications remain responsible for:
695
+
696
+ - TLS and trusted-proxy configuration;
697
+ - CSRF, CORS, and origin validation;
698
+ - login and renewal rate limiting;
699
+ - equivalent password verification work for unknown accounts;
700
+ - account status and authorization rules;
701
+ - re-authentication for sensitive actions;
702
+ - database migrations and cleanup;
703
+ - never logging passwords, raw tokens, cookie headers, or password hashes.
704
+
705
+ Exact IP/User-Agent/platform matching is defense in depth. It does not prevent every stolen-cookie replay scenario.