@gauts/auth 0.2.0 → 0.2.2

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 (45) hide show
  1. package/README.md +559 -169
  2. package/dist/adapters/hono/index.d.ts +11 -3
  3. package/dist/adapters/hono/index.d.ts.map +1 -1
  4. package/dist/adapters/hono/index.js +28 -10
  5. package/dist/adapters/hono/index.js.map +1 -1
  6. package/dist/adapters/prisma/index.d.ts +31 -0
  7. package/dist/adapters/prisma/index.d.ts.map +1 -0
  8. package/dist/adapters/prisma/index.js +119 -0
  9. package/dist/adapters/prisma/index.js.map +1 -0
  10. package/dist/adapters/redis/index.d.ts +5 -5
  11. package/dist/adapters/redis/index.d.ts.map +1 -1
  12. package/dist/adapters/redis/index.js +19 -19
  13. package/dist/adapters/redis/index.js.map +1 -1
  14. package/dist/auth.d.ts +4 -4
  15. package/dist/auth.d.ts.map +1 -1
  16. package/dist/auth.js +5 -5
  17. package/dist/auth.js.map +1 -1
  18. package/dist/client/index.d.ts +3 -3
  19. package/dist/client/index.d.ts.map +1 -1
  20. package/dist/client/index.js +17 -6
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/config.d.ts +1 -1
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +4 -11
  25. package/dist/config.js.map +1 -1
  26. package/dist/errors.d.ts +2 -2
  27. package/dist/errors.d.ts.map +1 -1
  28. package/dist/errors.js +7 -5
  29. package/dist/errors.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/password/index.d.ts.map +1 -1
  33. package/dist/password/index.js +2 -4
  34. package/dist/password/index.js.map +1 -1
  35. package/dist/session/schema.d.ts.map +1 -1
  36. package/dist/session/schema.js +10 -10
  37. package/dist/session/schema.js.map +1 -1
  38. package/dist/session/service.d.ts +4 -4
  39. package/dist/session/service.d.ts.map +1 -1
  40. package/dist/session/service.js +130 -112
  41. package/dist/session/service.js.map +1 -1
  42. package/dist/session/token.js.map +1 -1
  43. package/dist/session/types.d.ts +45 -42
  44. package/dist/session/types.d.ts.map +1 -1
  45. package/package.json +92 -92
package/README.md CHANGED
@@ -1,288 +1,678 @@
1
1
  # `@gauts/auth`
2
2
 
3
- Reusable password and opaque server-side session authentication for Node.js applications.
3
+ Reusable password authentication and opaque server-side sessions for Node.js applications.
4
4
 
5
- `@gauts/auth` validates sessions through Redis and records session history through an application-owned database adapter. It does not create endpoints, database models, users, or application authorization rules.
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.
6
6
 
7
- ## Design
7
+ ## Requirements
8
8
 
9
- - Argon2id by default, with explicit bcrypt support.
10
- - One stable 256-bit opaque browser token per session.
11
- - SHA-256 token hashes in Redis and the session records database.
12
- - Redis-authoritative authentication with no database fallback.
13
- - Sliding inactivity expiration without an absolute lifetime.
14
- - Configurable client validation, using the complete User-Agent by default.
15
- - Framework integrations through explicit package exports.
9
+ - 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.
16
13
 
17
14
  ## Installation
18
15
 
16
+ For Hono, Prisma, and Redis:
17
+
19
18
  ```bash
20
- npm install @gauts/auth redis hono
19
+ npm install @gauts/auth hono redis @prisma/client
21
20
  ```
22
21
 
23
- ## Setup
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.
24
23
 
25
- ```ts
26
- import { createAuth, type SessionRecords } from "@gauts/auth";
27
- import { createHonoAdapter } from "@gauts/auth/hono";
28
- import { createRedisStore } from "@gauts/auth/redis";
29
-
30
- const records: SessionRecords = {
31
- create: (session) => db.sessions.create(session),
32
- find: ({ accountId, sessionId }) =>
33
- db.sessions.find({ accountId, sessionId }),
34
- findActive: ({ accountId, now }) =>
35
- db.sessions.findActive({ accountId, now }),
36
- revoke: ({ revokedAt, sessionIds }) =>
37
- db.sessions.revoke({ revokedAt, sessionIds }),
38
- updateExpiry: ({ expiresAt, sessionId, updatedAt }) =>
39
- db.sessions.updateExpiry({ expiresAt, sessionId, updatedAt }),
40
- };
24
+ ## Flow
25
+
26
+ Login:
27
+
28
+ ```text
29
+ password -> configured algorithm -> database session row
30
+ -> Redis session
31
+ -> opaque HttpOnly cookie
32
+ ```
33
+
34
+ Authenticated HTTP request:
35
+
36
+ ```text
37
+ cookie -> SHA-256 hash -> Redis -> client validation -> route
38
+ -> renewal due -> database expiry
39
+ -> Redis TTL
40
+ -> cookie expiry
41
+ ```
42
+
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.
44
+
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.
41
46
 
47
+ ## Prisma model
48
+
49
+ The default Prisma model is `auth_sessions`:
50
+
51
+ ```prisma
52
+ model auth_sessions {
53
+ id String @id @default(uuid()) @db.VarChar(255)
54
+ account_id String @db.VarChar(255)
55
+ token_hash String @unique @db.VarChar(64)
56
+ ip String? @db.VarChar(45)
57
+ platform String? @db.VarChar(255)
58
+ agent String? @db.Text
59
+ expires_at DateTime @db.Timestamp(0)
60
+ revoked_at DateTime? @db.Timestamp(0)
61
+ created_at DateTime @default(now()) @db.Timestamp(0)
62
+ updated_at DateTime? @db.Timestamp(0)
63
+
64
+ @@index([account_id])
65
+ @@index([expires_at])
66
+ @@index([revoked_at])
67
+ }
68
+ ```
69
+
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.
71
+
72
+ Create migrations through the consuming application's normal Prisma workflow. The package never creates or runs migrations.
73
+
74
+ ## Quick start with Hono
75
+
76
+ ### 1. Define the session data
77
+
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.
79
+
80
+ ```ts
42
81
  type AccountSession = {
43
- email: string;
44
- role: "owner" | "admin";
82
+ email: string;
83
+ role: "admin" | "owner";
45
84
  };
85
+ ```
46
86
 
47
- const auth = createAuth<AccountSession>({
48
- redis: createRedisStore({ client: redis, config: { prefix: "my-app:auth" } }),
49
- records,
50
- session: {
51
- ttl: 60 * 60 * 24 * 7,
52
- renewInterval: 60 * 60 * 24,
53
- max: 10,
54
- validation: ["userAgent"],
55
- },
87
+ ### 2. Create one auth instance
88
+
89
+ ```ts
90
+ import { createHonoAuth } from "@gauts/auth/hono";
91
+ import { createDbAdapter } from "@gauts/auth/prisma";
92
+ import { createRedisAdapter } from "@gauts/auth/redis";
93
+
94
+ export const auth = createHonoAuth<AccountSession>({
95
+ getIp: (c) => getTrustedClientIp(c),
96
+ db: createDbAdapter({
97
+ client: prisma,
98
+ }),
99
+ redis: createRedisAdapter({
100
+ client: redis,
101
+ config: { prefix: "my-app:auth" },
102
+ }),
56
103
  });
104
+ ```
105
+
106
+ Both clients must already be initialized by the application. The package does not connect, reconnect, disconnect, or close them.
107
+
108
+ `createDbAdapter` uses `auth_sessions` by default. `config` is optional:
57
109
 
58
- const hono = createHonoAdapter({
59
- auth,
60
- getIp: (c) => getTrustedClientIp(c),
110
+ ```ts
111
+ const db = createDbAdapter({
112
+ client: prisma,
113
+ config: {
114
+ table: "admin_sessions",
115
+ },
61
116
  });
62
117
  ```
63
118
 
64
- The application must obtain the client IP according to its own trusted-proxy configuration. The package canonicalizes the supplied IPv4 or IPv6 value but never decides which forwarding headers are trusted.
119
+ When `table` is supplied, TypeScript only accepts a compatible model from that generated Prisma client.
120
+
121
+ ### 3. Type the Hono application
65
122
 
66
- ## Client information
123
+ ```ts
124
+ import { Hono } from "hono";
125
+ import type { HonoAuthEnv } from "@gauts/auth/hono";
67
126
 
68
- The application supplies `getIp` because only its deployment knows which proxies and forwarding
69
- headers are trusted. The Hono adapter reads User-Agent and platform directly from the request.
127
+ const app = new Hono<HonoAuthEnv<AccountSession>>();
128
+ ```
70
129
 
71
- The resulting client information has this shape:
130
+ `auth.requireSession` installs both values:
72
131
 
73
132
  ```ts
74
- type SessionClientInput = {
75
- ip?: string | null;
76
- userAgent?: string | null;
77
- platform?: string | null;
78
- };
133
+ const session = c.get("session");
134
+ const account = c.get("account");
79
135
  ```
80
136
 
81
- - `validation` selects which client fields are compared on authenticated requests.
82
- - `userAgent` is compared by default.
83
- - `ip` and `platform` are always stored as session metadata, even when they are not compared.
84
- - IPv4, IPv4-mapped IPv6, and IPv6 values are canonicalized by the package.
85
- - Platform is unquoted, trimmed, and limited to 255 characters.
86
- - User-Agent is stored and compared in full without truncation.
137
+ ### 4. Login
87
138
 
88
- `getIp` runs during session creation and on every authenticated request. It should only resolve the
89
- trusted request IP and must not perform unrelated account or database work.
139
+ The application owns request validation, account lookup, rate limiting, status checks, and error responses.
90
140
 
91
- ## Passwords
141
+ ```ts
142
+ app.post("/auth/login", async (c) => {
143
+ const { email, password } = await c.req.json<{
144
+ email: string;
145
+ password: string;
146
+ }>();
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
+ ) {
156
+ return c.json({ error: "Invalid credentials." }, 401);
157
+ }
158
+
159
+ const session = await auth.createSession({
160
+ account_id: account.id,
161
+ context: c,
162
+ data: {
163
+ email: account.email,
164
+ role: account.role,
165
+ },
166
+ });
167
+
168
+ return c.json({ account: session.data });
169
+ });
170
+ ```
92
171
 
93
- Argon2id is the default:
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.
173
+
174
+ ### 5. Protect routes
175
+
176
+ ```ts
177
+ app.get("/account", auth.requireSession, (c) => {
178
+ return c.json({
179
+ account: c.get("account"),
180
+ session_id: c.get("session").id,
181
+ });
182
+ });
183
+ ```
184
+
185
+ `requireSession` authenticates the request. It does not apply application roles or permissions.
186
+
187
+ ### 6. Logout
94
188
 
95
189
  ```ts
96
- const hash = await auth.password.hash(password);
97
- const valid = await auth.password.verify({ password, storedHash: hash });
190
+ app.post("/auth/logout", async (c) => {
191
+ await auth.revokeSession(c);
192
+ return c.body(null, 204);
193
+ });
98
194
  ```
99
195
 
100
- Applications whose database contains bcrypt hashes select bcrypt explicitly:
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.
197
+
198
+ ## Configuration
199
+
200
+ ### `createHonoAuth`
101
201
 
102
202
  ```ts
103
- const auth = createAuth({
104
- password: {
105
- algorithm: "bcrypt",
106
- },
107
- redis: createRedisStore({ client: redis }),
108
- records,
203
+ const auth = createHonoAuth<AccountSession>({
204
+ getIp,
205
+ db,
206
+ redis,
207
+ password,
208
+ session,
209
+ cookie,
109
210
  });
110
211
  ```
111
212
 
112
- New bcrypt hashes reject passwords above 72 bytes. Applications with historical hashes created from longer bcrypt input can opt into a larger verification-only boundary without permitting new truncated hashes:
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. |
221
+
222
+ Configuration is resolved once when the auth instance is created. Invalid configuration throws `AUTH_CONFIG_INVALID` during startup.
223
+
224
+ ### Password
225
+
226
+ Argon2id is the default:
227
+
228
+ ```ts
229
+ password: {
230
+ algorithm: "argon2id",
231
+ }
232
+ ```
233
+
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. |
242
+
243
+ Applications with bcrypt hashes must select bcrypt explicitly:
113
244
 
114
245
  ```ts
115
246
  password: {
116
247
  algorithm: "bcrypt",
117
- maxBytes: 72,
118
- verifyMaxBytes: 1024,
119
248
  }
120
249
  ```
121
250
 
122
- The configured algorithm is used for both hashing and verification. The package does not detect, convert, migrate, or fall back to another algorithm.
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. |
123
257
 
124
- ## Login
258
+ 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.
125
259
 
126
- The application owns the endpoint and account lookup:
260
+ bcrypt ignores bytes after the first 72. Keep `verifyMaxBytes` at `72` unless preserving historical truncation is an explicit application requirement.
261
+
262
+ ### Session
127
263
 
128
264
  ```ts
129
- app.post("/auth/login", async (c) => {
130
- const { email, password } = await c.req.json();
131
- const account = await findAccount(email);
132
-
133
- if (
134
- !account ||
135
- !(await auth.password.verify({
136
- password,
137
- storedHash: account.passwordHash,
138
- }))
139
- ) {
140
- return c.json({ error: "Invalid credentials." }, 401);
141
- }
142
-
143
- const session = await hono.createSession({
144
- accountId: account.id,
145
- context: c,
146
- data: {
147
- email: account.email,
148
- role: account.role,
149
- },
150
- });
265
+ session: {
266
+ max: 10,
267
+ renewInterval: 24 * 60 * 60,
268
+ ttl: 7 * 24 * 60 * 60,
269
+ validation: ["agent"],
270
+ }
271
+ ```
151
272
 
152
- return c.json({ account: session.data });
153
- });
273
+ Time values are seconds.
274
+
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. |
281
+
282
+ Expiry is sliding and has no forced absolute lifetime. The opaque token is not rotated during renewal.
283
+
284
+ ### Cookie
285
+
286
+ ```ts
287
+ cookie: {
288
+ domain: undefined,
289
+ name: "__Host-session",
290
+ path: "/",
291
+ sameSite: "Lax",
292
+ secure: true,
293
+ }
154
294
  ```
155
295
 
156
- Applications must apply their own login rate limiting and account-enumeration protection.
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. |
157
303
 
158
- ## Protected routes
304
+ `HttpOnly` is always enabled.
305
+
306
+ - `__Host-` requires `secure: true`, `path: "/"`, and no domain.
307
+ - `__Secure-` requires `secure: true`.
308
+ - `SameSite=None` requires `secure: true`.
309
+ - Local HTTP development with `secure: false` requires a custom name without a secure prefix.
310
+
311
+ ### Redis adapter
159
312
 
160
313
  ```ts
161
- app.get("/account", hono.requireSession, async (c) => {
162
- const session = c.get("session");
163
- const account = await findAccountById(session.accountId);
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();
164
319
 
165
- return c.json({ account });
320
+ const adapter = createRedisAdapter({
321
+ client: redis,
322
+ config: {
323
+ prefix: "my-app:auth",
324
+ },
166
325
  });
167
326
  ```
168
327
 
169
- The middleware reads the cookie, resolves Redis, compares the configured client fields, renews expiry when due, and places the typed session in the Hono context. It does not load application data from the database.
328
+ `config` is optional. The prefix defaults to `gauts:auth`, supports letters, numbers, `:`, `_`, and `-`, and has a maximum of 128 characters.
170
329
 
171
- Applications that need to populate additional Hono context variables can resolve through the same adapter without duplicating its cookie behavior:
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:
172
339
 
173
340
  ```ts
174
- const requireAccount = async (c, next) => {
175
- const session = await hono.resolveSession(c);
341
+ import { createDbAdapter } from "@gauts/auth/prisma";
176
342
 
177
- c.set("account", session.data);
178
- await next();
179
- };
343
+ const db = createDbAdapter({
344
+ client: prisma,
345
+ });
180
346
  ```
181
347
 
182
- ## Logout
348
+ Custom compatible model:
183
349
 
184
350
  ```ts
185
- app.post("/auth/logout", async (c) => {
186
- await hono.revokeSession(c);
187
- return c.body(null, 204);
351
+ const db = createDbAdapter({
352
+ client: prisma,
353
+ config: {
354
+ table: "admin_sessions",
355
+ },
188
356
  });
189
357
  ```
190
358
 
191
- The adapter clears the cookie only after backend revocation succeeds. Infrastructure errors are
192
- propagated so the application can return its own service-error response.
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.
362
+
363
+ Database failures are exposed by the auth core as `DB_UNAVAILABLE`.
193
364
 
194
- ## Active sessions
365
+ ### Custom database adapter
366
+
367
+ Applications not using Prisma can implement the exported `DbAdapter` contract:
195
368
 
196
369
  ```ts
197
- const sessions = await auth.session.list(accountId);
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
+ ```
198
380
 
199
- await auth.session.revoke({
200
- accountId,
201
- sessionId,
202
- });
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`.
203
382
 
204
- await auth.session.revokeAccount(accountId);
383
+ ### Trusted client IP
384
+
385
+ ```ts
386
+ getIp: (c) => getTrustedClientIp(c);
205
387
  ```
206
388
 
207
- The records database supplies session history. Redis is checked in a batch before a session is reported as active. Token hashes are never returned by `list`.
389
+ `getIp` runs during login and every authenticated HTTP request. It may be synchronous or asynchronous and returns `string`, `null`, or `undefined`.
390
+
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:
394
+
395
+ ```ts
396
+ type SessionClientInput = {
397
+ agent?: string | null;
398
+ ip?: string | null;
399
+ platform?: string | null;
400
+ };
401
+ ```
402
+
403
+ - IPv4, IPv4-mapped IPv6, and IPv6 are canonicalized.
404
+ - `::1` becomes `127.0.0.1`.
405
+ - 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
411
+
412
+ ### `auth.password.algorithm`
413
+
414
+ The resolved algorithm: `"argon2id"` or `"bcrypt"`.
208
415
 
209
- ## Synchronizing session data
416
+ ### `auth.password.hash(password)`
210
417
 
211
- When cached session claims change:
418
+ Validates the UTF-8 byte length and creates a hash using the configured algorithm.
212
419
 
213
420
  ```ts
214
- await auth.session.sync({
215
- accountId,
216
- data: {
217
- email: account.email,
218
- role: account.role,
219
- },
421
+ const password_hash = await auth.password.hash(password);
422
+ ```
423
+
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,
220
432
  });
221
433
  ```
222
434
 
223
- Synchronization preserves the existing token and Redis TTL.
435
+ Invalid password size throws `PASSWORD_INPUT_INVALID`. Wrong credentials return `false`.
436
+
437
+ ## Hono API
224
438
 
225
- ## Session records contract
439
+ ### `auth.createSession({ account_id, context, data })`
226
440
 
227
- Each consuming application maps `SessionRecords` to its database. The durable record contains:
441
+ Creates the database and Redis session, writes the HttpOnly cookie, and returns `Session<TData>`.
228
442
 
229
- - Session ID.
230
- - Account ID and relation owned by the application.
231
- - SHA-256 token hash.
232
- - Canonical IP.
233
- - Platform metadata.
234
- - Complete User-Agent.
235
- - Creation, current expiry, update, and revocation timestamps.
443
+ ### `auth.resolveSession(context)`
236
444
 
237
- The database never stores the raw browser token.
445
+ Reads and validates the cookie and returns `Session<TData>`.
238
446
 
239
- ## Expiration
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.
240
452
 
241
- Defaults:
453
+ ### `auth.requireSession`
454
+
455
+ Middleware that calls `resolveSession` and sets:
242
456
 
243
457
  ```ts
244
- session: {
245
- ttl: 7 * 24 * 60 * 60,
246
- renewInterval: 24 * 60 * 60,
247
- max: 10,
248
- validation: ["userAgent"],
458
+ c.set("session", session);
459
+ c.set("account", session.data);
460
+ ```
461
+
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:
481
+
482
+ ```ts
483
+ {
484
+ session: Session<TData>;
485
+ token: string;
249
486
  }
250
487
  ```
251
488
 
252
- - Inactive sessions expire after seven days.
253
- - Active sessions renew at most once every 24 hours.
254
- - Renewal keeps the same browser token.
255
- - There is no absolute session lifetime.
256
- - Every successful `resolve` call counts as activity and renews the session when due.
489
+ Prefer `auth.createSession` in Hono applications because the core cannot write the browser cookie.
490
+
491
+ ### `auth.session.resolve({ client, token })`
492
+
493
+ Validates and renews when due. It returns `null` for an invalid or expired session, otherwise:
494
+
495
+ ```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
+ ```
547
+
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
+ };
560
+ ```
257
561
 
258
562
  ## Client validation
259
563
 
260
- The default compares only the complete User-Agent:
564
+ The default compares the complete User-Agent:
565
+
566
+ ```ts
567
+ session: {
568
+ validation: ["agent"],
569
+ }
570
+ ```
571
+
572
+ An administration application can also bind IP and platform:
261
573
 
262
574
  ```ts
263
575
  session: {
264
- validation: ["userAgent"],
576
+ validation: ["ip", "agent", "platform"],
265
577
  }
266
578
  ```
267
579
 
268
- Applications can choose any combination of `ip`, `userAgent`, and `platform`:
580
+ An empty array validates only the opaque token:
269
581
 
270
582
  ```ts
271
583
  session: {
272
- validation: ["ip", "userAgent", "platform"],
584
+ validation: [],
273
585
  }
274
586
  ```
275
587
 
276
- An empty array disables client-field comparison and validates only the session token. Unknown or duplicate fields are rejected during startup.
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.
589
+
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.
591
+
592
+ ## Expiration and renewal
593
+
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.
277
602
 
278
- If a configured field differs, the package deletes the Redis session first and marks its database record as revoked. Clearing the response cookie is client cleanup; Redis deletion is what removes access.
603
+ ## Errors
279
604
 
280
- Exact IP validation is opt-in because VPN, mobile, and other networks can change a legitimate user's public IP. Platform validation is also opt-in because the client-hint header may be absent. Client-field validation is defense in depth and does not replace TLS, secure cookies, CSRF protection, XSS prevention, or re-authentication for sensitive actions.
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
+ ```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.
281
668
 
282
669
  ## Package exports
283
670
 
284
671
  ```text
285
- @gauts/auth createAuth and public core types
286
- @gauts/auth/redis createRedisStore
287
- @gauts/auth/hono createHonoAdapter
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
288
676
  ```
677
+
678
+ The compiled Hono example is available in [`examples/hono`](./examples/hono).