@gauts/auth 0.2.1 → 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.
- package/README.md +554 -175
- package/dist/adapters/hono/index.d.ts +11 -3
- package/dist/adapters/hono/index.d.ts.map +1 -1
- package/dist/adapters/hono/index.js +28 -10
- package/dist/adapters/hono/index.js.map +1 -1
- package/dist/adapters/prisma/index.d.ts +31 -0
- package/dist/adapters/prisma/index.d.ts.map +1 -0
- package/dist/adapters/prisma/index.js +119 -0
- package/dist/adapters/prisma/index.js.map +1 -0
- package/dist/adapters/redis/index.d.ts +5 -5
- package/dist/adapters/redis/index.d.ts.map +1 -1
- package/dist/adapters/redis/index.js +19 -19
- package/dist/adapters/redis/index.js.map +1 -1
- package/dist/auth.d.ts +4 -4
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +5 -5
- package/dist/auth.js.map +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +17 -6
- package/dist/client/index.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -11
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -5
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/password/index.d.ts.map +1 -1
- package/dist/password/index.js +2 -4
- package/dist/password/index.js.map +1 -1
- package/dist/session/schema.d.ts.map +1 -1
- package/dist/session/schema.js +10 -10
- package/dist/session/schema.js.map +1 -1
- package/dist/session/service.d.ts +4 -4
- package/dist/session/service.d.ts.map +1 -1
- package/dist/session/service.js +91 -85
- package/dist/session/service.js.map +1 -1
- package/dist/session/token.js.map +1 -1
- package/dist/session/types.d.ts +42 -40
- package/dist/session/types.d.ts.map +1 -1
- package/package.json +92 -92
package/README.md
CHANGED
|
@@ -1,299 +1,678 @@
|
|
|
1
1
|
# `@gauts/auth`
|
|
2
2
|
|
|
3
|
-
Reusable password and opaque server-side
|
|
3
|
+
Reusable password authentication and opaque server-side sessions for Node.js applications.
|
|
4
4
|
|
|
5
|
-
`@gauts/auth` validates sessions through Redis and
|
|
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
|
-
##
|
|
7
|
+
## Requirements
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
|
19
|
+
npm install @gauts/auth hono redis @prisma/client
|
|
21
20
|
```
|
|
22
21
|
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
82
|
+
email: string;
|
|
83
|
+
role: "admin" | "owner";
|
|
45
84
|
};
|
|
85
|
+
```
|
|
46
86
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
57
107
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
108
|
+
`createDbAdapter` uses `auth_sessions` by default. `config` is optional:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const db = createDbAdapter({
|
|
112
|
+
client: prisma,
|
|
113
|
+
config: {
|
|
114
|
+
table: "admin_sessions",
|
|
115
|
+
},
|
|
61
116
|
});
|
|
62
117
|
```
|
|
63
118
|
|
|
64
|
-
|
|
119
|
+
When `table` is supplied, TypeScript only accepts a compatible model from that generated Prisma client.
|
|
65
120
|
|
|
66
|
-
|
|
121
|
+
### 3. Type the Hono application
|
|
67
122
|
|
|
68
|
-
|
|
69
|
-
|
|
123
|
+
```ts
|
|
124
|
+
import { Hono } from "hono";
|
|
125
|
+
import type { HonoAuthEnv } from "@gauts/auth/hono";
|
|
126
|
+
|
|
127
|
+
const app = new Hono<HonoAuthEnv<AccountSession>>();
|
|
128
|
+
```
|
|
70
129
|
|
|
71
|
-
|
|
130
|
+
`auth.requireSession` installs both values:
|
|
72
131
|
|
|
73
132
|
```ts
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
137
|
+
### 4. Login
|
|
138
|
+
|
|
139
|
+
The application owns request validation, account lookup, rate limiting, status checks, and error responses.
|
|
140
|
+
|
|
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
|
+
```
|
|
87
171
|
|
|
88
|
-
`
|
|
89
|
-
trusted request IP and must not perform unrelated account or database work.
|
|
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.
|
|
90
173
|
|
|
91
|
-
|
|
174
|
+
### 5. Protect routes
|
|
92
175
|
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
190
|
+
app.post("/auth/logout", async (c) => {
|
|
191
|
+
await auth.revokeSession(c);
|
|
192
|
+
return c.body(null, 204);
|
|
193
|
+
});
|
|
98
194
|
```
|
|
99
195
|
|
|
100
|
-
|
|
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 =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
203
|
+
const auth = createHonoAuth<AccountSession>({
|
|
204
|
+
getIp,
|
|
205
|
+
db,
|
|
206
|
+
redis,
|
|
207
|
+
password,
|
|
208
|
+
session,
|
|
209
|
+
cookie,
|
|
109
210
|
});
|
|
110
211
|
```
|
|
111
212
|
|
|
112
|
-
|
|
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
|
-
|
|
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. |
|
|
257
|
+
|
|
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.
|
|
123
259
|
|
|
124
|
-
|
|
260
|
+
bcrypt ignores bytes after the first 72. Keep `verifyMaxBytes` at `72` unless preserving historical truncation is an explicit application requirement.
|
|
125
261
|
|
|
126
|
-
|
|
262
|
+
### Session
|
|
127
263
|
|
|
128
264
|
```ts
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
`HttpOnly` is always enabled.
|
|
157
305
|
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
320
|
+
const adapter = createRedisAdapter({
|
|
321
|
+
client: redis,
|
|
322
|
+
config: {
|
|
323
|
+
prefix: "my-app:auth",
|
|
324
|
+
},
|
|
166
325
|
});
|
|
167
326
|
```
|
|
168
327
|
|
|
169
|
-
|
|
328
|
+
`config` is optional. The prefix defaults to `gauts:auth`, supports letters, numbers, `:`, `_`, and `-`, and has a maximum of 128 characters.
|
|
170
329
|
|
|
171
|
-
|
|
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
|
-
|
|
175
|
-
const session = await hono.resolveSession(c);
|
|
341
|
+
import { createDbAdapter } from "@gauts/auth/prisma";
|
|
176
342
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
};
|
|
343
|
+
const db = createDbAdapter({
|
|
344
|
+
client: prisma,
|
|
345
|
+
});
|
|
180
346
|
```
|
|
181
347
|
|
|
182
|
-
|
|
348
|
+
Custom compatible model:
|
|
183
349
|
|
|
184
350
|
```ts
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
351
|
+
const db = createDbAdapter({
|
|
352
|
+
client: prisma,
|
|
353
|
+
config: {
|
|
354
|
+
table: "admin_sessions",
|
|
355
|
+
},
|
|
188
356
|
});
|
|
189
357
|
```
|
|
190
358
|
|
|
191
|
-
The adapter
|
|
192
|
-
|
|
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`.
|
|
364
|
+
|
|
365
|
+
### Custom database adapter
|
|
193
366
|
|
|
194
|
-
|
|
367
|
+
Applications not using Prisma can implement the exported `DbAdapter` contract:
|
|
195
368
|
|
|
196
369
|
```ts
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
383
|
+
### Trusted client IP
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
getIp: (c) => getTrustedClientIp(c);
|
|
205
387
|
```
|
|
206
388
|
|
|
207
|
-
|
|
389
|
+
`getIp` runs during login and every authenticated HTTP request. It may be synchronous or asynchronous and returns `string`, `null`, or `undefined`.
|
|
208
390
|
|
|
209
|
-
|
|
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.
|
|
210
392
|
|
|
211
|
-
|
|
393
|
+
The Hono adapter reads the remaining metadata from request headers:
|
|
212
394
|
|
|
213
395
|
```ts
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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"`.
|
|
415
|
+
|
|
416
|
+
### `auth.password.hash(password)`
|
|
417
|
+
|
|
418
|
+
Validates the UTF-8 byte length and creates a hash using the configured algorithm.
|
|
419
|
+
|
|
420
|
+
```ts
|
|
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
|
-
|
|
435
|
+
Invalid password size throws `PASSWORD_INPUT_INVALID`. Wrong credentials return `false`.
|
|
436
|
+
|
|
437
|
+
## Hono API
|
|
438
|
+
|
|
439
|
+
### `auth.createSession({ account_id, context, data })`
|
|
224
440
|
|
|
225
|
-
|
|
441
|
+
Creates the database and Redis session, writes the HttpOnly cookie, and returns `Session<TData>`.
|
|
226
442
|
|
|
227
|
-
|
|
443
|
+
### `auth.resolveSession(context)`
|
|
228
444
|
|
|
229
|
-
|
|
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.
|
|
445
|
+
Reads and validates the cookie and returns `Session<TData>`.
|
|
236
446
|
|
|
237
|
-
|
|
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.
|
|
238
452
|
|
|
239
|
-
|
|
453
|
+
### `auth.requireSession`
|
|
240
454
|
|
|
241
|
-
|
|
455
|
+
Middleware that calls `resolveSession` and sets:
|
|
242
456
|
|
|
243
457
|
```ts
|
|
244
|
-
session
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
- There is no absolute session lifetime.
|
|
256
|
-
- Every successful `resolve` call counts as HTTP 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 })`
|
|
257
492
|
|
|
258
|
-
|
|
493
|
+
Validates and renews when due. It returns `null` for an invalid or expired session, otherwise:
|
|
259
494
|
|
|
260
495
|
```ts
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
496
|
+
{
|
|
497
|
+
renewed: boolean;
|
|
498
|
+
session: Session<TData>;
|
|
499
|
+
}
|
|
265
500
|
```
|
|
266
501
|
|
|
267
|
-
|
|
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
|
+
```
|
|
268
561
|
|
|
269
562
|
## Client validation
|
|
270
563
|
|
|
271
|
-
The default compares
|
|
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:
|
|
272
573
|
|
|
273
574
|
```ts
|
|
274
575
|
session: {
|
|
275
|
-
validation: ["
|
|
576
|
+
validation: ["ip", "agent", "platform"],
|
|
276
577
|
}
|
|
277
578
|
```
|
|
278
579
|
|
|
279
|
-
|
|
580
|
+
An empty array validates only the opaque token:
|
|
280
581
|
|
|
281
582
|
```ts
|
|
282
583
|
session: {
|
|
283
|
-
validation: [
|
|
584
|
+
validation: [],
|
|
284
585
|
}
|
|
285
586
|
```
|
|
286
587
|
|
|
287
|
-
|
|
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.
|
|
602
|
+
|
|
603
|
+
## Errors
|
|
604
|
+
|
|
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
|
|
288
655
|
|
|
289
|
-
|
|
656
|
+
The package does not provide:
|
|
290
657
|
|
|
291
|
-
|
|
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.
|
|
292
668
|
|
|
293
669
|
## Package exports
|
|
294
670
|
|
|
295
671
|
```text
|
|
296
|
-
@gauts/auth
|
|
297
|
-
@gauts/auth/
|
|
298
|
-
@gauts/auth/
|
|
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
|
|
299
676
|
```
|
|
677
|
+
|
|
678
|
+
The compiled Hono example is available in [`examples/hono`](./examples/hono).
|