@gauts/auth 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +471 -198
  2. package/SECURITY.md +10 -2
  3. package/dist/adapters/hono/index.d.ts +28 -4
  4. package/dist/adapters/hono/index.d.ts.map +1 -1
  5. package/dist/adapters/hono/index.js +40 -7
  6. package/dist/adapters/hono/index.js.map +1 -1
  7. package/dist/adapters/hono/social.d.ts +34 -0
  8. package/dist/adapters/hono/social.d.ts.map +1 -0
  9. package/dist/adapters/hono/social.js +202 -0
  10. package/dist/adapters/hono/social.js.map +1 -0
  11. package/dist/adapters/prisma/config.d.ts.map +1 -1
  12. package/dist/adapters/prisma/config.js +85 -71
  13. package/dist/adapters/prisma/config.js.map +1 -1
  14. package/dist/adapters/prisma/index.d.ts +1 -1
  15. package/dist/adapters/prisma/index.d.ts.map +1 -1
  16. package/dist/adapters/prisma/index.js +90 -13
  17. package/dist/adapters/prisma/index.js.map +1 -1
  18. package/dist/adapters/prisma/model.d.ts +40 -7
  19. package/dist/adapters/prisma/model.d.ts.map +1 -1
  20. package/dist/adapters/prisma/model.js +38 -24
  21. package/dist/adapters/prisma/model.js.map +1 -1
  22. package/dist/adapters/prisma/types.d.ts +60 -72
  23. package/dist/adapters/prisma/types.d.ts.map +1 -1
  24. package/dist/errors.d.ts +1 -1
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js.map +1 -1
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/providers/common.d.ts +31 -0
  30. package/dist/providers/common.d.ts.map +1 -0
  31. package/dist/providers/common.js +89 -0
  32. package/dist/providers/common.js.map +1 -0
  33. package/dist/providers/github.d.ts +5 -0
  34. package/dist/providers/github.d.ts.map +1 -0
  35. package/dist/providers/github.js +93 -0
  36. package/dist/providers/github.js.map +1 -0
  37. package/dist/providers/google.d.ts +5 -0
  38. package/dist/providers/google.d.ts.map +1 -0
  39. package/dist/providers/google.js +58 -0
  40. package/dist/providers/google.js.map +1 -0
  41. package/dist/providers/index.d.ts +7 -0
  42. package/dist/providers/index.d.ts.map +1 -0
  43. package/dist/providers/index.js +4 -0
  44. package/dist/providers/index.js.map +1 -0
  45. package/dist/providers/x.d.ts +5 -0
  46. package/dist/providers/x.d.ts.map +1 -0
  47. package/dist/providers/x.js +63 -0
  48. package/dist/providers/x.js.map +1 -0
  49. package/dist/session/cookie.d.ts +4 -0
  50. package/dist/session/cookie.d.ts.map +1 -1
  51. package/dist/session/cookie.js +9 -3
  52. package/dist/session/cookie.js.map +1 -1
  53. package/dist/session/guards.d.ts.map +1 -1
  54. package/dist/session/guards.js +7 -1
  55. package/dist/session/guards.js.map +1 -1
  56. package/dist/session/types.d.ts +6 -0
  57. package/dist/session/types.d.ts.map +1 -1
  58. package/dist/social/config.d.ts +3 -0
  59. package/dist/social/config.d.ts.map +1 -0
  60. package/dist/social/config.js +115 -0
  61. package/dist/social/config.js.map +1 -0
  62. package/dist/social/service.d.ts +18 -0
  63. package/dist/social/service.d.ts.map +1 -0
  64. package/dist/social/service.js +78 -0
  65. package/dist/social/service.js.map +1 -0
  66. package/dist/social/state.d.ts +46 -0
  67. package/dist/social/state.d.ts.map +1 -0
  68. package/dist/social/state.js +139 -0
  69. package/dist/social/state.js.map +1 -0
  70. package/dist/social/types.d.ts +75 -0
  71. package/dist/social/types.d.ts.map +1 -0
  72. package/dist/social/types.js +2 -0
  73. package/dist/social/types.js.map +1 -0
  74. package/package.json +7 -2
package/README.md CHANGED
@@ -1,72 +1,144 @@
1
1
  # `@gauts/auth`
2
2
 
3
- Database-backed password authentication and opaque browser sessions for Node.js applications.
3
+ Database-backed password authentication, opaque browser sessions, and optional social authentication for Node.js applications.
4
4
 
5
- `@gauts/auth` provides the reusable authentication layer: password hashing, session lifecycle, secure cookies, database validation, optional short caching, and framework adapters. The application keeps control of registration, account lookup, authorization, routes, responses, and UI.
5
+ `@gauts/auth` provides password hashing, session lifecycle, secure cookies, database validation, optional short caching, Prisma persistence, Hono integration, and Google/GitHub/X OAuth. The application keeps control of credential lookup, business-specific registration data, authorization, responses, and UI.
6
6
 
7
7
  ## Features
8
8
 
9
- | Capability | Support | Default |
10
- | ---------------------------------- | :-----: | ------------------- |
11
- | Argon2id password hashing | ✅ | Enabled |
12
- | bcrypt password hashing | ✅ | Opt-in |
13
- | Opaque server-side sessions | ✅ | Enabled |
14
- | Database-backed validation | ✅ | Enabled |
15
- | Sliding session renewal | ✅ | Every 24 hours |
16
- | Absolute session lifetime | ✅ | 30 days |
17
- | Signed browser cache | ✅ | Disabled |
18
- | Full User-Agent validation | ✅ | Enabled |
19
- | IP validation | ✅ | Disabled |
20
- | Platform validation | ✅ | Disabled |
21
- | Hono adapter | ✅ | Available |
22
- | Prisma adapter | ✅ | Available |
23
- | Next.js renewal adapter | ✅ | Available |
24
- | Session listing and revocation | ✅ | Available |
25
- | Token rotation | | Stable opaque token |
26
- | JWT sessions | | Not used |
27
- | Redis requirement | | Not required |
28
- | Registration, OAuth, OTP, or email | | Application-owned |
29
- | Route roles and permissions | ❌ | Application-owned |
9
+ | Capability | Support | Default |
10
+ | ------------------------------ | :-----: | ------------------- |
11
+ | Argon2id password hashing | ✅ | Enabled |
12
+ | bcrypt password hashing | ✅ | Opt-in |
13
+ | Opaque server-side sessions | ✅ | Enabled |
14
+ | Database-backed validation | ✅ | Enabled |
15
+ | Sliding session renewal | ✅ | Every 24 hours |
16
+ | Absolute session lifetime | ✅ | 30 days |
17
+ | Signed browser cache | ✅ | Disabled |
18
+ | Full User-Agent validation | ✅ | Enabled |
19
+ | IP validation | ✅ | Disabled |
20
+ | Platform validation | ✅ | Disabled |
21
+ | Hono adapter | ✅ | Available |
22
+ | Prisma adapter | ✅ | Available |
23
+ | Next.js renewal adapter | ✅ | Available |
24
+ | Google social authentication | ✅ | Opt-in |
25
+ | GitHub social authentication | | Opt-in |
26
+ | X social authentication | | Opt-in |
27
+ | Social account registration | | Disabled |
28
+ | Session listing and revocation | | Available |
29
+ | Token rotation | ❌ | Stable opaque token |
30
+ | JWT sessions | ❌ | Not used |
31
+ | Redis requirement | ❌ | Not required |
32
+ | OTP and transactional email | ❌ | Application-owned |
33
+ | Route roles and permissions | ❌ | Application-owned |
30
34
 
31
35
  “Session renewal” extends the existing session expiry when activity continues. It is not a refresh-token flow and does not rotate the opaque browser token.
32
36
 
33
- ## Installation
34
-
35
- ### Requirements
37
+ ## Quick start
36
38
 
37
- - Node.js 22 or newer.
38
- - A database adapter.
39
- - Hono 4 when using the Hono adapter.
40
- - Next.js 15 or newer when using the Next.js adapter.
39
+ The application defines its credential login, renewal, logout, protected endpoints, and social route paths. Optional social authentication exposes the Hono handler used inside the application-owned route.
41
40
 
42
- ### Hono and Prisma
41
+ ### 1. Install the package
43
42
 
44
43
  ```bash
45
- npm install @gauts/auth hono @prisma/client
44
+ npm install @gauts/auth
46
45
  ```
47
46
 
48
- ### Next.js
47
+ Install the peer dependencies used by each application if they are not already present:
49
48
 
50
49
  ```bash
51
- npm install @gauts/auth next
50
+ npm install hono @prisma/client
51
+ npm install next
52
52
  ```
53
53
 
54
- `hono` and `next` are optional peer dependencies. The Prisma adapter receives the application's generated Prisma client and does not import Prisma at runtime.
54
+ ### 2. Add the Prisma schema
55
55
 
56
- ### Package entry points
56
+ The default adapter uses this fixed relationship tree:
57
57
 
58
- | Import | Purpose |
59
- | -------------------- | --------------------------------------------------------- |
60
- | `@gauts/auth` | Password service, session core, errors, and public types. |
61
- | `@gauts/auth/prisma` | Prisma database adapter. |
62
- | `@gauts/auth/hono` | Hono cookies, methods, and middleware. |
63
- | `@gauts/auth/next` | Next.js renewal scheduling and `Set-Cookie` forwarding. |
58
+ ```text
59
+ users
60
+ └── user_accounts
61
+ ├── account_sessions
62
+ └── social_accounts (optional)
63
+ ```
64
64
 
65
- ## Quick start
65
+ Add the three required models to the API schema. `password_hash` may be nullable when the same account model also supports social authentication.
66
+
67
+ ```prisma
68
+ model users {
69
+ id String @id @default(uuid()) @db.VarChar(255)
70
+ name String @db.VarChar(255)
71
+
72
+ accounts user_accounts[]
73
+ }
74
+
75
+ model user_accounts {
76
+ id String @id @default(uuid()) @db.VarChar(255)
77
+ user_id String @db.VarChar(255)
78
+ email String @unique @db.VarChar(255)
79
+ password_hash String? @db.VarChar(255)
80
+
81
+ user users @relation(fields: [user_id], references: [id], onDelete: Cascade)
82
+ sessions account_sessions[]
83
+
84
+ @@index([user_id])
85
+ }
86
+
87
+ model account_sessions {
88
+ id String @id @default(uuid()) @db.VarChar(255)
89
+ account_id String @db.VarChar(255)
90
+ token_hash String @unique @db.VarChar(64)
91
+ ip String? @db.VarChar(45)
92
+ country String? @db.VarChar(2)
93
+ platform String? @db.VarChar(255)
94
+ agent String? @db.Text
95
+ expires_at DateTime @db.Timestamp(0)
96
+ revoked_at DateTime? @db.Timestamp(0)
97
+ created_at DateTime @default(now()) @db.Timestamp(0)
98
+ updated_at DateTime? @db.Timestamp(0)
99
+
100
+ account user_accounts @relation(fields: [account_id], references: [id], onDelete: Cascade)
66
101
 
67
- The Hono adapter does not create routes automatically. The application defines its own login, renewal, logout, and protected endpoints.
102
+ @@index([account_id])
103
+ @@index([expires_at])
104
+ @@index([revoked_at])
105
+ }
106
+ ```
68
107
 
69
- ### 1. Create the auth instance
108
+ When social authentication is enabled, add this relation inside `user_accounts`:
109
+
110
+ ```prisma
111
+ socials social_accounts[]
112
+ ```
113
+
114
+ Then add the optional model:
115
+
116
+ ```prisma
117
+ model social_accounts {
118
+ id String @id @default(uuid()) @db.VarChar(255)
119
+ account_id String @db.VarChar(255)
120
+ provider String @db.VarChar(32)
121
+ provider_id String @db.VarChar(255)
122
+ created_at DateTime @default(now()) @db.Timestamp(0)
123
+
124
+ account user_accounts @relation(fields: [account_id], references: [id], onDelete: Cascade)
125
+
126
+ @@unique([provider, provider_id])
127
+ @@unique([account_id, provider])
128
+ @@index([account_id])
129
+ }
130
+ ```
131
+
132
+ Create the migration through the application's Prisma workflow, then regenerate its client:
133
+
134
+ ```bash
135
+ npx prisma migrate dev --name add_auth
136
+ npx prisma generate
137
+ ```
138
+
139
+ ### 3. Create the auth instance and routes
140
+
141
+ Create the API auth instance:
70
142
 
71
143
  ```ts
72
144
  import { createHonoAuth } from "@gauts/auth/hono";
@@ -93,7 +165,7 @@ cache disabled
93
165
  cookies __ses, __cac, __ren
94
166
  ```
95
167
 
96
- ### 2. Add the routes
168
+ Mount the application-owned login routes and the package middleware:
97
169
 
98
170
  ```ts
99
171
  import { Hono } from "hono";
@@ -145,41 +217,82 @@ app.get("/account", auth.requireSession, (c) => {
145
217
  return c.json({
146
218
  account: c.get("account"),
147
219
  session: c.get("session"),
220
+ user: c.get("user"),
148
221
  });
149
222
  });
150
223
  ```
151
224
 
152
225
  When `storedHash` is missing, the package performs password work with the configured algorithm and always returns `false`. Applications do not need a dummy hash. Keep the response identical for unknown accounts and incorrect passwords.
153
226
 
154
- ### 3. Enable the optional cache
227
+ ### 4. Connect the Next.js frontend
228
+
229
+ Create the renewal adapter with the API's private URL:
155
230
 
156
231
  ```ts
157
- export const auth = createHonoAuth({
158
- cache: {
159
- ttl: 60,
160
- },
161
- db,
162
- secret: requiredEnv("AUTH_SECRET"),
232
+ import { createNextAuth } from "@gauts/auth/next";
233
+
234
+ export const nextAuth = createNextAuth({
235
+ renewUrl: `${process.env.NEXT_PRIVATE_API_URL}/auth/renew`,
163
236
  });
164
237
  ```
165
238
 
166
- `AUTH_SECRET` must contain at least 32 high-entropy bytes. It stays in the API and is never shared with Next.js.
239
+ Call it from the Next.js middleware (proxy.ts) on protected routes:
240
+
241
+ ```ts
242
+ import type { NextRequest } from "next/server";
243
+ import { NextResponse } from "next/server";
244
+
245
+ import { nextAuth } from "./lib/auth.js";
167
246
 
168
- `requiredEnv()` represents an application helper that returns a non-empty environment string or fails during startup.
247
+ export const proxy = async (request: NextRequest) => {
248
+ const response = NextResponse.next();
249
+ const renewal = await nextAuth.renew({ request, response });
250
+
251
+ if (renewal.status === 401) {
252
+ return NextResponse.redirect(new URL("/auth/login", request.url));
253
+ }
254
+
255
+ if (renewal.status !== null && renewal.status >= 500) {
256
+ return NextResponse.redirect(new URL("/maintenance", request.url));
257
+ }
258
+
259
+ return renewal.response;
260
+ };
261
+ ```
262
+
263
+ The frontend does not receive `AUTH_SECRET`. The API remains responsible for session validation and every `Set-Cookie` response.
264
+
265
+ ## Requirements and package entry points
266
+
267
+ - Node.js 22 or newer.
268
+ - A database adapter.
269
+ - Hono 4 when using the Hono adapter.
270
+ - Next.js 15 or newer when using the Next.js adapter.
271
+
272
+ `hono` and `next` are optional peer dependencies. The Prisma adapter receives the application's generated Prisma client and does not import Prisma at runtime.
273
+
274
+ | Import | Purpose |
275
+ | ----------------------- | --------------------------------------------------------- |
276
+ | `@gauts/auth` | Password service, session core, errors, and public types. |
277
+ | `@gauts/auth/prisma` | Prisma database adapter. |
278
+ | `@gauts/auth/hono` | Hono cookies, methods, and middleware. |
279
+ | `@gauts/auth/next` | Next.js renewal scheduling and `Set-Cookie` forwarding. |
280
+ | `@gauts/auth/providers` | Google, GitHub, and X OAuth providers. |
169
281
 
170
282
  ## Configuration reference
171
283
 
172
284
  ### `createHonoAuth()`
173
285
 
174
- | Property | Type / allowed values | Required | Default | Description |
175
- | ---------- | --------------------- | :---------------------: | ----------------- | --------------------------------------------------------------------------------------------------- |
176
- | `db` | `DbAdapter` | ✅ | — | Authoritative session persistence and account loading. |
177
- | `getIp` | `HonoGetIp` | Only with IP validation | Omitted | Returns the client IP from a source trusted by the application. May be synchronous or asynchronous. |
178
- | `password` | `PasswordConfig` | ❌ | Argon2id defaults | Password hashing and verification configuration. |
179
- | `session` | `SessionConfig` | ❌ | Session defaults | Expiry, renewal, and client validation configuration. |
180
- | `cookie` | `HonoCookieConfig` | ❌ | Cookie defaults | Names, domain, path, SameSite, and Secure settings. |
181
- | `cache` | `{ ttl: number }` | ❌ | Disabled | Enables the short signed browser cache. |
182
- | `secret` | `string` | When `cache` is enabled | — | HMAC secret for the signed cache. Minimum 32 UTF-8 bytes. |
286
+ | Property | Type / allowed values | Required | Default | Description |
287
+ | ---------- | --------------------- | :----------------------: | ----------------- | --------------------------------------------------------------------------------------------------- |
288
+ | `db` | `DbAdapter` | ✅ | — | Authoritative session persistence and account loading. |
289
+ | `getIp` | `HonoGetIp` | Only with IP validation | Omitted | Returns the client IP from a source trusted by the application. May be synchronous or asynchronous. |
290
+ | `password` | `PasswordConfig` | ❌ | Argon2id defaults | Password hashing and verification configuration. |
291
+ | `session` | `SessionConfig` | ❌ | Session defaults | Expiry, renewal, and client validation configuration. |
292
+ | `cookie` | `HonoCookieConfig` | ❌ | Cookie defaults | Names, domain, path, SameSite, and Secure settings. |
293
+ | `cache` | `{ ttl: number }` | ❌ | Disabled | Enables the short signed browser cache. |
294
+ | `secret` | `string` | With `cache` or `social` | — | HMAC secret for signed authentication data. Minimum 32 UTF-8 bytes. |
295
+ | `social` | `SocialConfig` | ❌ | Disabled | Enables configured social providers, redirects, and optional registration. |
183
296
 
184
297
  ```ts
185
298
  type HonoGetIp = (c: Context) => Promise<string | null | undefined> | string | null | undefined;
@@ -351,7 +464,22 @@ The cache is signed but not encrypted. Do not place passwords, password hashes,
351
464
 
352
465
  ### Prisma adapter
353
466
 
354
- The default adapter requires the Prisma models `sessions` and `account`. The session model must expose an `account` relation. The default account model must contain string `id`, `email`, and `password_hash` fields. Custom account models require string `id` and `email`; their password column remains application-defined because the session adapter never reads it. Only `account.id` and `account.email` enter the payload by default.
467
+ The Prisma adapter uses one fixed, predictable session relationship tree and one optional social relation:
468
+
469
+ ```text
470
+ users
471
+ └── user_accounts
472
+ ├── account_sessions
473
+ └── social_accounts (optional)
474
+ ```
475
+
476
+ The required default delegate names are `prisma.users`, `prisma.user_accounts`, and `prisma.account_sessions`. When `prisma.social_accounts` exists, the adapter adds social persistence automatically. The fixed Prisma relation fields are:
477
+
478
+ - `user_accounts.user`;
479
+ - `account_sessions.account`;
480
+ - `social_accounts.account` when social persistence is present.
481
+
482
+ There is no relation mapping configuration. Applications may rename delegates with `table`, add payload fields with `select`, and define access conditions with `access`. Omitting the social model removes the social methods from the adapter without affecting password or session authentication.
355
483
 
356
484
  #### Default models
357
485
 
@@ -367,6 +495,10 @@ The resolved account payload is:
367
495
  {
368
496
  email: "owner@example.com",
369
497
  id: "account-id",
498
+ user: {
499
+ id: "user-id",
500
+ name: "Company name",
501
+ },
370
502
  }
371
503
  ```
372
504
 
@@ -378,84 +510,65 @@ The resolved account payload is:
378
510
  const db = createPrismaAdapter({
379
511
  client: prisma,
380
512
  models: {
381
- account: {
382
- select: ["id", "email", "name", "role"],
513
+ accounts: {
514
+ select: ["name", "role", "status", "timezone"],
383
515
  access: {
384
516
  role: ["OWNER", "ADMIN"],
385
517
  status: ["ACTIVE"],
386
518
  },
387
519
  },
520
+ users: {
521
+ select: ["role", "status"],
522
+ access: {
523
+ status: ["ACTIVE", "PENDING"],
524
+ },
525
+ },
388
526
  },
389
527
  });
390
528
  ```
391
529
 
392
- `status` is selected internally for validation but is not exposed because it is absent from `select`.
530
+ Fields used by `access` are selected internally. They enter the returned/cached payload only when they are also present in `select`.
393
531
 
394
- #### Custom models and nested relations
532
+ #### Custom delegate names
395
533
 
396
- Use `name` when the Prisma delegate differs from the default. Nested relation keys must match the relation fields on the parent model:
534
+ Use `table` only when a Prisma delegate differs from the default:
397
535
 
398
536
  ```ts
399
537
  const db = createPrismaAdapter({
400
538
  client: prisma,
401
539
  models: {
540
+ accounts: {
541
+ table: "admin_accounts",
542
+ select: ["name", "role", "status"],
543
+ },
402
544
  sessions: {
403
- name: "admin_sessions",
545
+ table: "admin_sessions",
404
546
  },
405
- account: {
406
- name: "user_accounts",
407
- select: ["id", "email", "name", "role", "status", "timezone"],
408
- access: {
409
- role: ["OWNER", "ADMIN"],
410
- status: ["ACTIVE"],
411
- },
412
- relations: {
413
- user: {
414
- name: "users",
415
- select: ["id", "role", "status"],
416
- access: {
417
- status: ["ACTIVE"],
418
- },
419
- },
420
- },
547
+ // Only when social authentication is enabled.
548
+ socials: {
549
+ table: "admin_social_accounts",
421
550
  },
422
- },
423
- });
424
- ```
425
-
426
- This configuration uses `prisma.admin_sessions`, `prisma.user_accounts`, and `prisma.users`. The root relation on `admin_sessions` must still be named `account`.
427
-
428
- When a relation name differs from its target model, the object key identifies the relation and `name` identifies the delegate:
429
-
430
- ```ts
431
- relations: {
432
- owner: {
433
- name: "users",
434
- select: ["id", "email", "status"],
435
- access: {
436
- status: ["ACTIVE"],
551
+ users: {
552
+ table: "admin_users",
553
+ select: ["role", "status"],
437
554
  },
438
555
  },
439
- }
556
+ });
440
557
  ```
441
558
 
442
- This loads the `account.owner` relation using the `prisma.users` model metadata.
443
-
444
- | Property | Type / allowed values | Required | Default | Description |
445
- | ------------------------------------- | ----------------------------------------- | :------: | -------------------- | --------------------------------------------------------------------------- |
446
- | `client` | Generated Prisma client | | | Prisma client containing every configured model. |
447
- | `models.sessions.name` | Compatible session delegate name | ❌ | `"sessions"` | Overrides the Prisma model used to persist sessions. |
448
- | `models.account.name` | Account delegate with string `id`, `email` | ❌ | `"account"` | Identifies a custom account model. Its password column may use any name. |
449
- | `models.account.select` | Unique scalar field array | ❌ | `["id", "email"]` | Fields exposed in `account` and the signed cache. `id` is always included. |
450
- | `models.account.access` | Scalar equality or allowed-value arrays | ❌ | `{}` | Conditions required for authentication. |
451
- | `models.account.relations` | Relation configuration object | ❌ | `{}` | Additional required to-one relations loaded inside `account`. |
452
- | `models.account.relations[key].name` | Compatible related-model delegate name | ❌ | Relation key | Identifies the related model and types its `select` and `access`. |
453
- | `models.account.relations[key].select`| Unique scalar field array | ❌ | `["id"]` | Fields exposed under that relation. `id` is always included. |
454
- | `models.account.relations[key].access`| Scalar equality or allowed-value arrays | ❌ | `{}` | Conditions required on the related record. |
559
+ | Property | Type / allowed values | Required | Default | Description |
560
+ | ------------------------ | --------------------------------------- | :------: | -------------------- | ---------------------------------------------------------- |
561
+ | `client` | Generated Prisma client | ✅ | | Prisma client containing the three required auth models. |
562
+ | `models.users.table` | Compatible user delegate name | ❌ | `"users"` | Overrides the user delegate. |
563
+ | `models.users.select` | Unique scalar field array | | `[]` | Adds payload fields; `id` and `name` are always included. |
564
+ | `models.users.access` | Scalar equality or allowed-value arrays | ❌ | `{}` | Conditions required on the owning user/entity. |
565
+ | `models.accounts.table` | Compatible account delegate name | ❌ | `"user_accounts"` | Overrides the account delegate. |
566
+ | `models.accounts.select` | Unique scalar field array | ❌ | `[]` | Adds payload fields; `id` and `email` are always included. |
567
+ | `models.accounts.access` | Scalar equality or allowed-value arrays | ❌ | `{}` | Conditions required on the authenticating account. |
568
+ | `models.sessions.table` | Compatible session delegate name | ❌ | `"account_sessions"` | Overrides authoritative session persistence. |
569
+ | `models.socials.table` | Compatible social delegate name | ❌ | `"social_accounts"` | Overrides optional provider association persistence. |
455
570
 
456
- The root Prisma relation on the session model is always named `account`. Keys inside `relations` are the actual Prisma relation field names. `name` identifies the target Prisma model/delegate; it does not rename a relation or a physical SQL table.
457
-
458
- `select` accepts only JSON-safe scalar fields. It controls the public account payload and receives autocomplete from the generated Prisma client. Treat it as an explicit allowlist: never select passwords, password hashes, tokens, or other secrets because the cache is signed, not encrypted. Fields used only by `access` are selected for validation and removed before the account is exposed or cached.
571
+ `select` accepts JSON-safe scalar fields and receives autocomplete from the generated Prisma client. `password`, `hash`, `password_hash`, and `passwordHash` are rejected by both TypeScript and runtime validation. Never select tokens or other secrets because the optional cache is signed, not encrypted.
459
572
 
460
573
  Each `access` condition is either an exact scalar value or an array of accepted values:
461
574
 
@@ -466,7 +579,7 @@ access: {
466
579
  }
467
580
  ```
468
581
 
469
- All configured conditions and nested relations must match. Omitting `access` applies no application-specific account restriction.
582
+ Every configured account and user condition must match. Omitting `access` applies no application-specific account restriction.
470
583
 
471
584
  ### Next.js adapter
472
585
 
@@ -492,8 +605,8 @@ export const nextAuth = createNextAuth({
492
605
  credentials accepted
493
606
  -> generate 256-bit opaque token
494
607
  -> store SHA-256 token hash in DB
495
- -> load selected current account data and configured relations
496
- -> apply configured access rules
608
+ -> load current account and owning user
609
+ -> apply configured account/user access rules
497
610
  -> write __ses
498
611
  -> write __ren
499
612
  -> optionally write __cac
@@ -516,7 +629,7 @@ session token
516
629
  session token
517
630
  -> SHA-256 hash
518
631
  -> indexed DB lookup
519
- -> validate expiry, revocation, account access, configured relations, and client
632
+ -> validate expiry, revocation, account/user access, and client
520
633
  -> clear short cache
521
634
  -> continue
522
635
  ```
@@ -534,61 +647,6 @@ Next reads __ren
534
647
 
535
648
  `auth.session.resolve()` is always DB-backed and read-only. Only explicit renewal updates database expiry.
536
649
 
537
- ## Prisma schema
538
-
539
- The default Prisma adapter resolves:
540
-
541
- ```text
542
- sessions -> account
543
- ```
544
-
545
- The following is a complete MySQL/MariaDB example for email/password authentication. The account model requires `id`, `email`, and `password_hash`; the password hash is used only by the application's login query and never enters the session payload.
546
-
547
- ```prisma
548
- model account {
549
- id String @id @default(uuid()) @db.VarChar(255)
550
- email String @unique @db.VarChar(255)
551
- password_hash String @db.VarChar(255)
552
-
553
- sessions sessions[]
554
- }
555
-
556
- model sessions {
557
- id String @id @default(uuid()) @db.VarChar(255)
558
- account_id String @db.VarChar(255)
559
- token_hash String @unique @db.VarChar(64)
560
- ip String? @db.VarChar(45)
561
- country String? @db.VarChar(2)
562
- platform String? @db.VarChar(255)
563
- agent String? @db.Text
564
- expires_at DateTime @db.Timestamp(0)
565
- revoked_at DateTime? @db.Timestamp(0)
566
- created_at DateTime @default(now()) @db.Timestamp(0)
567
- updated_at DateTime? @db.Timestamp(0)
568
-
569
- account account @relation(fields: [account_id], references: [id], onDelete: Cascade)
570
-
571
- @@index([account_id])
572
- @@index([expires_at])
573
- @@index([revoked_at])
574
- }
575
- ```
576
-
577
- Required fields and relation names:
578
-
579
- | Path | Required fields |
580
- | ------------------ | ------------------------------------------------------------------------------------------------------------------- |
581
- | Session model | `id`, `account_id`, `token_hash`, `ip`, `platform`, `agent`, `expires_at`, `revoked_at`, `created_at`, `updated_at` |
582
- | `account` relation | String `id`, `email`, and `password_hash` |
583
-
584
- The session relation must be named `account`. Additional account fields and nested relations are selected through `models.account`. Application models may add fields, indexes, defaults, mappings, and relations. Access fields may use Prisma enums, strings, booleans, numbers, or null.
585
-
586
- Keep `agent` large enough for the complete User-Agent. Use provider-compatible native annotations when the database is not MySQL/MariaDB.
587
-
588
- `models.sessions.name` and every model `name` are Prisma client delegate names. Physical SQL table names remain an application concern and can use Prisma `@@map` without changing the adapter configuration.
589
-
590
- Create and run migrations through the application's Prisma workflow. The package never manages migrations.
591
-
592
650
  ## Hono adapter
593
651
 
594
652
  ### Request values
@@ -598,11 +656,17 @@ Create and run migrations through the application's Prisma workflow. The package
598
656
  ```ts
599
657
  const account = c.get("account");
600
658
  const session = c.get("session");
659
+ const user = c.get("user");
601
660
  ```
602
661
 
603
662
  ```ts
604
663
  type AuthAccount = {
664
+ email: string;
605
665
  id: string;
666
+ user: {
667
+ id: string;
668
+ name: string;
669
+ };
606
670
  };
607
671
 
608
672
  type Session = {
@@ -619,9 +683,9 @@ type Session = {
619
683
  };
620
684
  ```
621
685
 
622
- The Prisma adapter refines `AuthAccount` with the exact fields and nested relations declared in `select`.
686
+ The Prisma adapter refines `account` and `user` with the exact additional scalar fields declared in their respective `select` arrays.
623
687
 
624
- Only `account_id` is copied from the account payload into the session row. Current selected account data and configured relations are loaded through the database relation and never copied into the table.
688
+ Only `account_id` is copied into the session row. Current account/user data is loaded through the fixed database relations and is never duplicated in the session table.
625
689
 
626
690
  ### Login country metadata
627
691
 
@@ -639,18 +703,199 @@ await auth.createSession({
639
703
 
640
704
  ### Methods
641
705
 
642
- | Method | Purpose |
643
- | --------------------------------------------- | -------------------------------------------------------------------------- |
706
+ | Method | Purpose |
707
+ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
644
708
  | `auth.createSession({ account_id, context, country? })` | Creates the DB session and writes the browser cookies. `country` is optional login-time metadata. |
645
- | `auth.resolveSession(context)` | Resolves a request and returns the selected account and session. |
646
- | `auth.renewSession(context)` | Performs DB validation, renews when due, and writes authoritative cookies. |
647
- | `auth.revokeSession(context)` | Revokes the current DB session and clears cookies. |
648
- | `auth.clearSession(context)` | Clears browser cookies without revoking the DB session. |
649
- | `auth.getToken(context)` | Returns the validated opaque token from the request cookie. |
650
- | `auth.requireSession` | Hono middleware that authenticates and populates the context. |
709
+ | `auth.resolveSession(context)` | Resolves a request and returns the selected account and session. |
710
+ | `auth.renewSession(context)` | Performs DB validation, renews when due, and writes authoritative cookies. |
711
+ | `auth.revokeSession(context)` | Revokes the current DB session and clears cookies. |
712
+ | `auth.clearSession(context)` | Clears browser cookies without revoking the DB session. |
713
+ | `auth.getToken(context)` | Returns the validated opaque token from the request cookie. |
714
+ | `auth.requireSession` | Hono middleware that authenticates and populates the context. |
651
715
 
652
716
  `requireSession` authenticates only. Application-specific route permissions remain the application's responsibility.
653
717
 
718
+ ## Social authentication
719
+
720
+ Social authentication is disabled unless `social` is configured. Import providers separately so applications only include the providers they use:
721
+
722
+ Social authentication requires the optional `social_accounts` model documented in the schema setup. Without that model, `createPrismaAdapter()` remains a session-only adapter and configuring `social` fails during application startup.
723
+
724
+ ```ts
725
+ import { createHonoAuth } from "@gauts/auth/hono";
726
+ import { createPrismaAdapter } from "@gauts/auth/prisma";
727
+ import { github, google, x } from "@gauts/auth/providers";
728
+
729
+ export const auth = createHonoAuth({
730
+ db: createPrismaAdapter({ client: prisma }),
731
+ secret: requiredEnv("AUTH_SECRET"),
732
+ social: {
733
+ errorUrl: "https://app.example.com/auth/login",
734
+ providers: [
735
+ google({
736
+ callbackUrl: "https://app.example.com/proxy/auth/social/google/callback",
737
+ clientId: requiredEnv("GOOGLE_CLIENT_ID"),
738
+ clientSecret: requiredEnv("GOOGLE_CLIENT_SECRET"),
739
+ }),
740
+ github({
741
+ callbackUrl: "https://app.example.com/proxy/auth/social/github/callback",
742
+ clientId: requiredEnv("GITHUB_CLIENT_ID"),
743
+ clientSecret: requiredEnv("GITHUB_CLIENT_SECRET"),
744
+ }),
745
+ x({
746
+ callbackUrl: "https://app.example.com/proxy/auth/social/x/callback",
747
+ clientId: requiredEnv("X_CLIENT_ID"),
748
+ clientSecret: requiredEnv("X_CLIENT_SECRET"),
749
+ }),
750
+ ],
751
+ successUrl: "https://app.example.com/dashboard",
752
+ },
753
+ });
754
+ ```
755
+
756
+ Declare one application route for every configured provider and supported action:
757
+
758
+ ```ts
759
+ app.get("/auth/social/:provider/:action", async (c) => {
760
+ return auth.social.handle(c);
761
+ });
762
+ ```
763
+
764
+ The same route handles the explicit `start` and `callback` paths:
765
+
766
+ ```text
767
+ GET /proxy/auth/social/google/start?intent=login
768
+ GET /proxy/auth/social/google/start?intent=register
769
+ GET /proxy/auth/social/google/callback
770
+ GET /proxy/auth/social/github/start?intent=login
771
+ GET /proxy/auth/social/github/callback
772
+ GET /proxy/auth/social/x/start?intent=login
773
+ GET /proxy/auth/social/x/callback
774
+ ```
775
+
776
+ `intent` defaults to `login`. Only `start` and `callback` are accepted as actions. The public `/proxy` path in this example is expected to forward to the Hono `/auth` path. Provider callback URLs must exactly match the public callback paths registered with each provider.
777
+
778
+ The package does not create or mount routes. Applications may wrap `auth.social.handle(c)` with their own logging, metrics, rate limiting, or other route-level behavior.
779
+
780
+ ### Social configuration
781
+
782
+ | Property | Type / allowed values | Required | Default | Description |
783
+ | ---------------------------- | ------------------------------------ | :---------: | --------- | -------------------------------------------------------------------------- |
784
+ | `social.providers` | `SocialProvider[]` | ✅ | — | Configured Google, GitHub, or X providers. |
785
+ | `social.successUrl` | Absolute HTTP(S) URL | ✅ | — | Fixed redirect after session creation. |
786
+ | `social.errorUrl` | Absolute HTTP(S) URL | ✅ | — | Fixed redirect for expected provider/authentication failures. |
787
+ | `social.cookieName` | Valid cookie name | ❌ | `"__soc"` | Signed temporary OAuth/registration transaction cookie. |
788
+ | `social.registration` | `SocialRegistrationConfig` | ❌ | Disabled | Enables default or application-specific social registration. |
789
+ | `registration.registerUrl` | Absolute HTTP(S) URL | ❌ | Direct | Defers account creation to an application form. |
790
+ | `registration.createAccount` | Async callback returning `accountId` | Conditional | Built-in | Creates required business data when the default structure is insufficient. |
791
+
792
+ Provider configuration:
793
+
794
+ | Property | Type | Required | Description |
795
+ | -------------- | -------------------- | :------: | ------------------------------------------------------- |
796
+ | `clientId` | Non-empty string | ✅ | Public OAuth client identifier. |
797
+ | `clientSecret` | Non-empty string | ✅ | Server-only OAuth client secret. |
798
+ | `callbackUrl` | Absolute HTTP(S) URL | ✅ | Exact public callback URL registered with the provider. |
799
+
800
+ The normalized verified identity is:
801
+
802
+ ```ts
803
+ type SocialIdentity = {
804
+ avatarUrl: string | null;
805
+ email: string;
806
+ name: string;
807
+ provider: "google" | "github" | "x";
808
+ providerId: string;
809
+ username: string | null;
810
+ };
811
+ ```
812
+
813
+ Provider access/refresh tokens and raw provider profiles are never exposed to application callbacks or persisted.
814
+
815
+ ### Registration modes
816
+
817
+ Login only (default):
818
+
819
+ ```ts
820
+ social: {
821
+ errorUrl,
822
+ providers: [googleProvider],
823
+ successUrl,
824
+ }
825
+ ```
826
+
827
+ Default registration creates `users { id, name }`, `user_accounts { id, email, user_id }`, the provider link, and the session:
828
+
829
+ ```ts
830
+ social: {
831
+ errorUrl,
832
+ providers: [googleProvider],
833
+ registration: {},
834
+ successUrl,
835
+ }
836
+ ```
837
+
838
+ This requires every additional application column on `users` and `user_accounts` to be nullable or have a database default.
839
+
840
+ For additional required form data, configure a registration URL and callback:
841
+
842
+ ```ts
843
+ import type { SocialRegistrationInput } from "@gauts/auth";
844
+
845
+ type RegisterData = {
846
+ companyNumber: string;
847
+ };
848
+
849
+ social: {
850
+ errorUrl,
851
+ providers: [googleProvider],
852
+ registration: {
853
+ registerUrl: "https://app.example.com/auth/register/social",
854
+ createAccount: async ({ data, identity }: SocialRegistrationInput<RegisterData>) => {
855
+ const account = await createApplicationAccount({
856
+ companyNumber: data.companyNumber,
857
+ email: identity.email,
858
+ name: identity.name,
859
+ });
860
+
861
+ return { accountId: account.id };
862
+ },
863
+ },
864
+ successUrl,
865
+ }
866
+ ```
867
+
868
+ The application endpoint reads the verified identity and completes registration:
869
+
870
+ ```ts
871
+ app.get("/auth/register/social", (c) => {
872
+ return c.json(auth.social.getRegistration(c));
873
+ });
874
+
875
+ app.post("/auth/register/social", async (c) => {
876
+ const data = await c.req.json<{ companyNumber: string }>();
877
+
878
+ await auth.social.completeRegistration({
879
+ context: c,
880
+ data,
881
+ });
882
+
883
+ return c.json({ registered: true });
884
+ });
885
+ ```
886
+
887
+ `registerUrl` requires `createAccount`; otherwise submitted application data would have no owner. `createAccount` must create the fixed `users`/`user_accounts` relation and return the created account ID. The package then creates `social_accounts` and the authenticated session.
888
+
889
+ ### OAuth security
890
+
891
+ - Authorization Code flow with PKCE `S256` is used for every provider.
892
+ - State and the PKCE verifier live in the signed, HttpOnly `__soc` cookie for at most 10 minutes.
893
+ - The temporary cookie is cleared after success or expected failure.
894
+ - Only provider-verified email addresses are accepted.
895
+ - Provider IDs, not email addresses, are the stable social link identifiers.
896
+ - Redirect URLs come only from startup configuration; request query parameters cannot choose them.
897
+ - Social OAuth requires `SameSite=Lax` or `SameSite=None`; `Strict` fails during startup.
898
+
654
899
  ### Core and adapter composition
655
900
 
656
901
  `createHonoAuth()` is the normal entry point. Use separate composition only when the same core instance is required outside Hono:
@@ -759,11 +1004,27 @@ const db = {
759
1004
 
760
1005
  `findToken` receives only the SHA-256 token hash. It must return the current selected account plus an `allowed` result. Raw tokens must never be persisted.
761
1006
 
1007
+ Custom adapters used with `social` must additionally implement `SocialDbAdapter`:
1008
+
1009
+ ```ts
1010
+ import type { SocialDbAdapter } from "@gauts/auth";
1011
+
1012
+ const socialDb = {
1013
+ createAccount: async ({ email, name }) => accountId,
1014
+ createSocial: async (record) => {},
1015
+ findAccount: async (account_id) => null,
1016
+ findEmail: async (email) => null,
1017
+ findSocial: async ({ provider, provider_id }) => null,
1018
+ } satisfies SocialDbAdapter;
1019
+ ```
1020
+
1021
+ The Prisma adapter already implements both `DbAdapter` and `SocialDbAdapter`.
1022
+
762
1023
  ## Performance
763
1024
 
764
1025
  The package contains no Redis or in-process cache.
765
1026
 
766
- Without the optional browser cache, each `requireSession` performs an indexed database lookup through `sessions.token_hash`.
1027
+ Without the optional browser cache, each `requireSession` performs an indexed database lookup through `account_sessions.token_hash`.
767
1028
 
768
1029
  With a valid cache, `GET` and `HEAD` skip the lookup until `cache.ttl` expires. Unsafe methods always use current database state.
769
1030
 
@@ -774,6 +1035,12 @@ The tradeoff is explicit: revocation and selected account or relation changes ma
774
1035
  ```ts
775
1036
  type AuthErrorCode =
776
1037
  | "AUTH_CONFIG_INVALID"
1038
+ | "SOCIAL_ACCOUNT_INVALID"
1039
+ | "SOCIAL_ACCOUNT_NOT_FOUND"
1040
+ | "SOCIAL_EMAIL_INVALID"
1041
+ | "SOCIAL_PROVIDER_ERROR"
1042
+ | "SOCIAL_REGISTRATION_INVALID"
1043
+ | "SOCIAL_STATE_INVALID"
777
1044
  | "PASSWORD_INPUT_INVALID"
778
1045
  | "SESSION_CLIENT_MISMATCH"
779
1046
  | "SESSION_DATA_INVALID"
@@ -784,15 +1051,21 @@ type AuthErrorCode =
784
1051
 
785
1052
  Use `isAuthError(error)` before reading `error.code`.
786
1053
 
787
- | Code | Suggested HTTP status | Meaning |
788
- | ------------------------- | --------------------: | ----------------------------------------------------------------- |
789
- | `AUTH_CONFIG_INVALID` | `500` | Invalid startup configuration. |
790
- | `PASSWORD_INPUT_INVALID` | `400` | Password input violates configured limits. |
791
- | `SESSION_CLIENT_MISMATCH` | `403` | A configured client field does not match; the session is revoked. |
792
- | `SESSION_DATA_INVALID` | `400` | Invalid session or renewal data. |
793
- | `SESSION_INVALID` | `401` | Missing, expired, revoked, or unknown session. |
794
- | `SESSION_NOT_FOUND` | `404` | Requested session does not exist for the account. |
795
- | `DB_UNAVAILABLE` | `503` | Database operation failed. Authentication fails closed. |
1054
+ | Code | Suggested HTTP status | Meaning |
1055
+ | ----------------------------- | --------------------: | ----------------------------------------------------------------- |
1056
+ | `AUTH_CONFIG_INVALID` | `500` | Invalid startup configuration. |
1057
+ | `SOCIAL_ACCOUNT_INVALID` | `403` | Linked account or owning user failed configured access rules. |
1058
+ | `SOCIAL_ACCOUNT_NOT_FOUND` | `401` | Provider identity is not linked and registration is unavailable. |
1059
+ | `SOCIAL_EMAIL_INVALID` | `400` | Provider did not return a verified usable email address. |
1060
+ | `SOCIAL_PROVIDER_ERROR` | `401` | Provider denied or failed the OAuth exchange. |
1061
+ | `SOCIAL_REGISTRATION_INVALID` | `400` | Custom registration did not return a valid account ID. |
1062
+ | `SOCIAL_STATE_INVALID` | `400` | OAuth/registration state is missing, altered, or expired. |
1063
+ | `PASSWORD_INPUT_INVALID` | `400` | Password input violates configured limits. |
1064
+ | `SESSION_CLIENT_MISMATCH` | `403` | A configured client field does not match; the session is revoked. |
1065
+ | `SESSION_DATA_INVALID` | `400` | Invalid session or renewal data. |
1066
+ | `SESSION_INVALID` | `401` | Missing, expired, revoked, or unknown session. |
1067
+ | `SESSION_NOT_FOUND` | `404` | Requested session does not exist for the account. |
1068
+ | `DB_UNAVAILABLE` | `503` | Database operation failed. Authentication fails closed. |
796
1069
 
797
1070
  The package throws typed errors but does not choose application HTTP responses.
798
1071