@dunx/auth 2.5.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -217
- package/dist/auth.d.ts +6 -13
- package/dist/context.d.ts +8 -13
- package/dist/drizzle.d.ts +6 -23
- package/dist/guard.d.ts +8 -12
- package/dist/handler.d.ts +13 -29
- package/dist/index.js +3 -9
- package/dist/module.d.ts +10 -20
- package/dist/openapi.d.ts +7 -20
- package/dist/password.d.ts +7 -14
- package/dist/redis.d.ts +9 -17
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,251 +1,83 @@
|
|
|
1
1
|
# @dunx/auth
|
|
2
2
|
|
|
3
|
-
[Better Auth](https://better-auth.com) for
|
|
4
|
-
|
|
5
|
-
wiring: a module that builds the instance from your `ConfigService`, five routes that
|
|
6
|
-
mount its handler, a guard that composes with the `@Public()` and `@Roles()` metadata
|
|
7
|
-
`@dunx/http` already carries, and two adapters that let it drive Bun's own APIs.
|
|
3
|
+
[Better Auth](https://better-auth.com) for
|
|
4
|
+
[dunx](https://github.com/petarzarkov/dunx).
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
**This package is not an authentication system.** better-auth is, and it is very
|
|
7
|
+
good at it. This is the wiring: a module that builds the instance from your
|
|
8
|
+
`ConfigService`, five routes that mount its handler, a guard that composes with
|
|
9
|
+
the `@Public()` and `@Roles()` metadata `@dunx/http` already carries, and two
|
|
10
|
+
adapters that let it drive Bun's own APIs.
|
|
11
|
+
|
|
12
|
+
There is no dunx sign-in flow, no dunx session table and no dunx OAuth. Each is
|
|
13
|
+
a better-auth feature reached through `AuthModule.forRoot`'s options, which
|
|
14
|
+
**are** better-auth's `BetterAuthOptions`.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
13
17
|
|
|
14
18
|
```bash
|
|
15
19
|
bun add @dunx/auth better-auth
|
|
16
20
|
```
|
|
17
21
|
|
|
18
|
-
`
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
dunx OAuth. Every one of those is a better-auth feature reached through
|
|
25
|
-
`AuthModule.forRoot`'s options, which **are** better-auth's `BetterAuthOptions`. Its
|
|
26
|
-
documentation is the documentation.
|
|
27
|
-
|
|
28
|
-
## What dunx adds
|
|
29
|
-
|
|
30
|
-
| Export | What it is |
|
|
31
|
-
| --------------------- | --------------------------------------------------------------------- |
|
|
32
|
-
| `AuthModule` | `forRoot` / `forRootAsync`, binding the instance and mounting it |
|
|
33
|
-
| `Auth` | The injection token for the better-auth instance |
|
|
34
|
-
| `SessionGuard` | Middleware: authenticates, then reads `@Public()` and `@Roles()` |
|
|
35
|
-
| `AuthContext` | The authenticated caller, reachable from any service in the request |
|
|
36
|
-
| `Principal` | `{ session, user }` - better-auth's own inferred session type |
|
|
37
|
-
| `bunPassword` | `Bun.password` bcrypt in place of better-auth's JavaScript scrypt |
|
|
38
|
-
| `redisStorage` | `secondaryStorage` over `Bun.RedisClient` |
|
|
39
|
-
| `drizzleDatabase` | `database` over the drizzle handle `@dunx/infra/db` already opened |
|
|
40
|
-
| `rolesOf` | The `admin` plugin's `role` column read as a list |
|
|
41
|
-
| `AuthOptions` | The resolved options, the `basePath` and where the handler mounted |
|
|
42
|
-
|
|
43
|
-
## Getting started
|
|
22
|
+
`better-auth` is a **required** peer: this package imports `betterAuth` as a
|
|
23
|
+
value and cannot load without it. `drizzle-orm` is an optional peer, needed only
|
|
24
|
+
by `@dunx/auth/drizzle` - its own subpath so a Prisma, Kysely or MongoDB app
|
|
25
|
+
never loads it.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
44
28
|
|
|
45
29
|
```ts
|
|
46
30
|
import { AuthModule } from '@dunx/auth';
|
|
47
31
|
import { drizzleDatabase } from '@dunx/auth/drizzle';
|
|
48
|
-
import { Module } from '@dunx/core';
|
|
49
32
|
import { DbConnection } from '@dunx/infra/db';
|
|
50
|
-
import { admin, bearer } from 'better-auth/plugins';
|
|
51
33
|
|
|
52
34
|
@Module({
|
|
53
35
|
imports: [
|
|
54
36
|
AuthModule.forRootAsync({
|
|
37
|
+
imports: [DatabaseModule],
|
|
38
|
+
inject: [AppConfigService, DbConnection],
|
|
55
39
|
useFactory: (config: AppConfigService, connection: DbConnection) => ({
|
|
56
40
|
secret: config.get('auth').secret,
|
|
57
|
-
|
|
41
|
+
basePath: '/api/auth',
|
|
58
42
|
database: drizzleDatabase(connection),
|
|
59
43
|
emailAndPassword: { enabled: true },
|
|
60
|
-
plugins: [admin(), bearer()],
|
|
61
44
|
}),
|
|
62
|
-
inject: [AppConfigService, DbConnection] as const,
|
|
63
45
|
}),
|
|
64
46
|
],
|
|
65
47
|
})
|
|
66
|
-
export class
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
That is the whole integration. `forRoot(options)` is the same thing without a factory,
|
|
70
|
-
for when the secret is not behind config.
|
|
71
|
-
|
|
72
|
-
`forRootAsync` exists for the one reason it exists on `LoggerModule`, `DbModule` and
|
|
73
|
-
the rest: a zero-argument function cannot read `ConfigService`. It is not a second
|
|
74
|
-
mechanism - dunx settles every async factory before the first constructor runs, so
|
|
75
|
-
the instance is built and the connection handshaked before anything can ask for
|
|
76
|
-
either.
|
|
77
|
-
|
|
78
|
-
### The database tables
|
|
79
|
-
|
|
80
|
-
**dunx ships no schema for better-auth's tables.** They are better-auth's, they change
|
|
81
|
-
with the plugins you enable, and its own CLI generates them:
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
bunx @better-auth/cli generate
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Put the result in the schema object you already hand `@dunx/infra/db`, and
|
|
88
|
-
`drizzleDatabase(connection)` needs no schema argument - `@dunx/infra/db` builds its
|
|
89
|
-
handle with `drizzle({ client, schema })`, and better-auth's adapter reads
|
|
90
|
-
`db._.fullSchema` off it. `examples/full/src/database/auth.schema.ts` is a
|
|
91
|
-
generated schema in place.
|
|
92
|
-
|
|
93
|
-
A framework carrying its own copy of a library's tables is a copy that rots against
|
|
94
|
-
the library that reads them.
|
|
95
|
-
|
|
96
|
-
## Mounting
|
|
97
|
-
|
|
98
|
-
`AuthHandler` puts better-auth's `(request: Request) => Promise<Response>` behind five
|
|
99
|
-
wildcard routes - `GET`, `POST`, `PUT`, `PATCH` and `DELETE` at `<basePath>/*`.
|
|
100
|
-
`Bun.serve` matches a wildcard natively, so **Bun is still the router**: dunx does not
|
|
101
|
-
restate, wrap or re-dispatch a single better-auth endpoint, and the `Response` comes
|
|
102
|
-
back untouched, `Set-Cookie` headers and redirects included.
|
|
103
|
-
|
|
104
|
-
`basePath` is better-auth's own option, defaulting to `/api/auth`.
|
|
105
|
-
|
|
106
|
-
### With `setGlobalPrefix`
|
|
107
|
-
|
|
108
|
-
better-auth resolves an endpoint by comparing the **whole pathname** to its
|
|
109
|
-
`basePath`, so a global prefix makes the mount and the base path two different
|
|
110
|
-
strings for one URL:
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
// app.setGlobalPrefix('api') turns the `/auth` route into `/api/auth`.
|
|
114
|
-
AuthModule.forRootAsync({ useFactory: () => ({ basePath: '/api/auth', ... }) }, '/auth');
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
The second argument is the **route** path; `basePath` is what the browser sees. Get it
|
|
118
|
-
wrong and the first request through the handler fails with an `AuthError` naming both
|
|
119
|
-
paths, rather than better-auth quietly answering 404 to everything.
|
|
120
|
-
|
|
121
|
-
## The guard
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
// Global - every route needs a session unless it says otherwise.
|
|
125
|
-
HttpFactory.create(root, { middleware: [SessionGuard] });
|
|
126
|
-
|
|
127
|
-
// or scoped - this controller needs one, nothing else does.
|
|
128
|
-
@UseGuards(SessionGuard)
|
|
129
|
-
@Controller('profile')
|
|
130
|
-
class ProfileController {}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
`AuthModule` registers `SessionGuard` as a provider either way. It resolves the
|
|
134
|
-
session through better-auth's own `api.getSession`, so a cookie and the `bearer`
|
|
135
|
-
plugin's `Authorization: Bearer <token>` both work, and then reads the metadata
|
|
136
|
-
`@dunx/http` already had:
|
|
48
|
+
export class AuthFeatureModule {}
|
|
137
49
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
and a sign-in endpoint that required a session could never be reached.
|
|
141
|
-
- **`@Roles('admin', 'editor')`** - a 403 unless the caller holds one of them.
|
|
142
|
-
`@dunx/openapi` already reads the same key for its security schemes.
|
|
143
|
-
|
|
144
|
-
A public route that wants to *adapt* to an optional caller asks better-auth itself:
|
|
145
|
-
|
|
146
|
-
```ts
|
|
147
|
-
const principal = await this.auth.api.getSession({ headers: req.headers });
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
One line, and it keeps a session lookup off every public request in the app.
|
|
151
|
-
|
|
152
|
-
## Reaching the caller
|
|
153
|
-
|
|
154
|
-
`AuthContext` is `AsyncLocalStorage`, so a service three constructor hops from the
|
|
155
|
-
route sees the principal without it being threaded through a signature:
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
export class Audit {
|
|
159
|
-
constructor(private readonly auth: AuthContext) {}
|
|
160
|
-
|
|
161
|
-
entries(): readonly string[] {
|
|
162
|
-
const { user } = this.auth.require(); // 401 if there is none
|
|
163
|
-
return this.log.forUser(user.id);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
`current()` returns `Principal | undefined`; `require()` throws a 401.
|
|
169
|
-
|
|
170
|
-
Two alternatives were rejected. Request-scoped DI was measured and turned down
|
|
171
|
-
(`docs/ARCHITECTURE.md`), and hanging the principal off `req` reaches a route handler
|
|
172
|
-
but nothing a route handler calls. `AsyncLocalStorage` is a Node built-in Bun
|
|
173
|
-
implements natively, and it is already how `@dunx/core` carries request state.
|
|
174
|
-
|
|
175
|
-
It is a **second** store rather than a key in `RequestContext`, because that one is
|
|
176
|
-
the log record - every field in it is serialized into every line the request writes,
|
|
177
|
-
so a session object there would be noise on each entry and a redaction hazard in the
|
|
178
|
-
ones that matter. `userId` does go there, so every log line inside a
|
|
179
|
-
guarded request is already correlated to the user.
|
|
180
|
-
|
|
181
|
-
### Plugin types
|
|
182
|
-
|
|
183
|
-
`Auth` is generic over the options it was built from, the same trick
|
|
184
|
-
`@dunx/infra/db` uses for drizzle's schema: the token is the erased class, the type
|
|
185
|
-
argument rides on the annotation.
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
export const authOptions = { plugins: [admin()], ... } as const;
|
|
189
|
-
|
|
190
|
-
// `api` here has the admin plugin's endpoints on it.
|
|
191
|
-
constructor(private readonly auth: Auth<typeof authOptions>) {}
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Written bare, `Auth` carries better-auth's core endpoints only.
|
|
195
|
-
|
|
196
|
-
## Password hashing
|
|
197
|
-
|
|
198
|
-
better-auth's default hasher is **pure-JavaScript scrypt**. `AuthModule` replaces it
|
|
199
|
-
with `bunPassword` - native bcrypt through `Bun.password` - whenever
|
|
200
|
-
`emailAndPassword` is enabled and you did not supply a `password` of your own. That is
|
|
201
|
-
The rule is simple: if Bun ships it, use Bun.
|
|
202
|
-
|
|
203
|
-
Bun pre-hashes the input, so bcrypt's 72-byte cap is a non-issue even for a
|
|
204
|
-
maximum-length multibyte password, and `verify` reads a hash from another algorithm as
|
|
205
|
-
a clean authentication failure rather than a 500.
|
|
206
|
-
|
|
207
|
-
**Migrating an existing user table?** Those users' scrypt hashes will no longer verify
|
|
208
|
-
and they will have to reset their passwords. Pass your own `emailAndPassword.password`
|
|
209
|
-
to keep the old hasher, or a hybrid that tries both.
|
|
210
|
-
|
|
211
|
-
## Sessions in Redis
|
|
212
|
-
|
|
213
|
-
```ts
|
|
214
|
-
import { redisStorage } from '@dunx/auth';
|
|
215
|
-
|
|
216
|
-
AuthModule.forRootAsync({
|
|
217
|
-
useFactory: (redis: RedisConnection) => ({
|
|
218
|
-
secondaryStorage: redisStorage(redis),
|
|
219
|
-
...
|
|
220
|
-
}),
|
|
221
|
-
inject: [RedisConnection] as const,
|
|
222
|
-
});
|
|
50
|
+
// Globally, and opt routes out with @Public():
|
|
51
|
+
HttpFactory.create(AppModule, { middleware: [SessionGuard] });
|
|
223
52
|
```
|
|
224
53
|
|
|
225
|
-
|
|
226
|
-
costing a database round trip per request.
|
|
54
|
+
## What is here
|
|
227
55
|
|
|
228
|
-
|
|
229
|
-
`increment` are optional in better-auth's interface because most clients cannot do
|
|
230
|
-
them atomically - `Bun.RedisClient` can, through `GETDEL` and `INCR`. Without them
|
|
231
|
-
better-auth falls back to read-then-delete for single-use credentials, which is a
|
|
232
|
-
race, and to a non-atomic rate-limit counter.
|
|
56
|
+
The [Authentication guide](../../docs/guide/17-authentication.md) is canonical.
|
|
233
57
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
58
|
+
| Export | What it does |
|
|
59
|
+
| --------------------- | ----------------------------------------------------------------- |
|
|
60
|
+
| `AuthModule` | Builds the instance, mounts the handler, binds the guard |
|
|
61
|
+
| `Auth` | The better-auth instance, injectable |
|
|
62
|
+
| `SessionGuard` | Authenticates, honours `@Public()` and `@Roles()` |
|
|
63
|
+
| `AuthContext` | The authenticated caller, anywhere in the request |
|
|
64
|
+
| `betterAuthDocument` | better-auth's own paths merged into the OpenAPI document |
|
|
65
|
+
| `bunPassword` | `Bun.password` native bcrypt, applied by default |
|
|
66
|
+
| `@dunx/auth/drizzle` | better-auth over the connection the app already opened |
|
|
67
|
+
| `@dunx/auth/redis` | `secondaryStorage` over `Bun.RedisClient` |
|
|
237
68
|
|
|
238
|
-
##
|
|
69
|
+
## Notes
|
|
239
70
|
|
|
240
|
-
|
|
71
|
+
- dunx ships no schema for better-auth's tables. They are better-auth's, they
|
|
72
|
+
change with its plugins, and `bunx @better-auth/cli generate` writes them.
|
|
73
|
+
Export them under the singular model names the adapter looks up.
|
|
74
|
+
- Under `setGlobalPrefix`, `basePath` is what better-auth matches and `mountAt`
|
|
75
|
+
is where the route is mounted. Omitting `mountAt` with a non-default
|
|
76
|
+
`basePath` is a boot error.
|
|
77
|
+
- `AuthContext` is a second `AsyncLocalStorage` store rather than a key in
|
|
78
|
+
`RequestContext`, because everything in that store is serialized into every
|
|
79
|
+
log line the request writes.
|
|
241
80
|
|
|
242
|
-
|
|
243
|
-
| ------------- | --------------------------------------------------------------- |
|
|
244
|
-
| `AuthOptions` | The resolved options, the `basePath`, and the mount path |
|
|
245
|
-
| `Auth` | The better-auth instance |
|
|
246
|
-
| `AuthContext` | The per-request principal store |
|
|
247
|
-
| `SessionGuard`| The guard, ready for `middleware: [...]` or `@UseGuards` |
|
|
81
|
+
## License
|
|
248
82
|
|
|
249
|
-
|
|
250
|
-
`@dunx/transform`'s transform to have run - `@dunx/auth` works in an app with no
|
|
251
|
-
preload.
|
|
83
|
+
MIT
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import type { Auth as Instance, BetterAuthOptions } from 'better-auth';
|
|
2
2
|
/**
|
|
3
3
|
* The injection token for the better-auth instance, and the whole of dunx's
|
|
4
|
-
* contract with the library.
|
|
4
|
+
* contract with the library. `betterAuth()` returns a plain object, so there is no
|
|
5
|
+
* class to use: this is an abstract class whose members alias better-auth's own,
|
|
6
|
+
* which a real instance satisfies structurally.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* restatements - which a real instance satisfies structurally. That is what makes
|
|
10
|
-
* `constructor(private readonly auth: Auth)` work, since `@dunx/transform` records
|
|
11
|
-
* the bare type name and the container resolves it.
|
|
12
|
-
*
|
|
13
|
-
* The type argument is the `DbModule` trick from `@dunx/infra/db`: the token is the
|
|
14
|
-
* erased class, so `Auth<typeof authOptions>` at an injection site keeps the
|
|
15
|
-
* plugin-widened `api` while still resolving the one binding. Written bare, `Auth`
|
|
16
|
-
* carries better-auth's core endpoints only - a plugin's endpoints are on the
|
|
17
|
-
* annotation, not on the token.
|
|
8
|
+
* The type argument is `DbModule`'s trick - the token is the erased class, so
|
|
9
|
+
* `Auth<typeof authOptions>` keeps the plugin-widened `api` while resolving the
|
|
10
|
+
* one binding. Written bare it carries better-auth's core endpoints only.
|
|
18
11
|
*/
|
|
19
12
|
export declare abstract class Auth<O extends BetterAuthOptions = BetterAuthOptions> {
|
|
20
13
|
/**
|
package/dist/context.d.ts
CHANGED
|
@@ -2,21 +2,16 @@ import { RequestContext } from '@dunx/core';
|
|
|
2
2
|
import type { BetterAuthOptions } from 'better-auth';
|
|
3
3
|
import type { Principal } from './auth.js';
|
|
4
4
|
/**
|
|
5
|
-
* How the authenticated caller reaches a handler
|
|
6
|
-
* however deep.
|
|
5
|
+
* How the authenticated caller reaches a handler, and anything the handler calls.
|
|
7
6
|
*
|
|
8
|
-
* `AsyncLocalStorage`,
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* (docs/ARCHITECTURE.md), and hanging the principal off `req` reaches a route
|
|
13
|
-
* handler but nothing a route handler calls.
|
|
7
|
+
* `AsyncLocalStorage`, the only mechanism that gets a value from middleware to a
|
|
8
|
+
* service three constructor hops away without passing it. Request-scoped DI was
|
|
9
|
+
* measured and rejected; hanging the principal off `req` reaches a route handler
|
|
10
|
+
* but nothing it calls.
|
|
14
11
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* ones that matter. What does go there is `userId` - a well-known `RequestFields`
|
|
19
|
-
* key - so the log lines are correlated without carrying the principal.
|
|
12
|
+
* A second store rather than a key in `RequestContext`: that one is the log
|
|
13
|
+
* record, so a session object there would be noise on every line and a redaction
|
|
14
|
+
* hazard. `userId` does go there, so lines correlate without the principal.
|
|
20
15
|
*/
|
|
21
16
|
export declare class AuthContext {
|
|
22
17
|
#private;
|
package/dist/drizzle.d.ts
CHANGED
|
@@ -14,9 +14,9 @@ export interface DrizzleSource {
|
|
|
14
14
|
readonly db: unknown;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* better-auth's `database` option over a connection the app already opened
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* better-auth's `database` option over a connection the app already opened, so the
|
|
18
|
+
* app keeps one pool and one shutdown path rather than better-auth opening a
|
|
19
|
+
* second. Nothing here connects.
|
|
20
20
|
*
|
|
21
21
|
* ```ts
|
|
22
22
|
* AuthModule.forRootAsync({
|
|
@@ -27,29 +27,12 @@ export interface DrizzleSource {
|
|
|
27
27
|
* });
|
|
28
28
|
* ```
|
|
29
29
|
*
|
|
30
|
-
* The `provider` comes from the connection's
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* the adapter reads `db._.fullSchema`.
|
|
34
|
-
*
|
|
35
|
-
* **The tables have to be exported under the names better-auth looks up**, which is
|
|
36
|
-
* the singular model name and not the table name. The adapter does `fullSchema['user']`,
|
|
37
|
-
* so a barrel exporting `users` fails on the first query rather than at boot:
|
|
38
|
-
*
|
|
39
|
-
* ```
|
|
40
|
-
* BetterAuthError: [# Drizzle Adapter]: The model "user" was not found in the schema object.
|
|
41
|
-
* ```
|
|
42
|
-
*
|
|
43
|
-
* Either name the exports `user`, `session`, `account` and `verification`, or map them
|
|
44
|
-
* where the schema is assembled:
|
|
30
|
+
* The `provider` comes from the connection's dialect and the schema off
|
|
31
|
+
* `db._.fullSchema`. Tables must be exported under better-auth's singular model
|
|
32
|
+
* names, so a barrel exporting `users` fails on first query:
|
|
45
33
|
*
|
|
46
34
|
* ```ts
|
|
47
35
|
* schema: { user: users, session: sessions, account: accounts, verification: verifications }
|
|
48
36
|
* ```
|
|
49
|
-
*
|
|
50
|
-
* dunx ships **no** schema for those tables. They are better-auth's, they change with
|
|
51
|
-
* its plugins, and its own CLI generates them: `bunx @better-auth/cli generate`. A
|
|
52
|
-
* copy of them inside a framework is a copy that silently rots against the library
|
|
53
|
-
* that reads it.
|
|
54
37
|
*/
|
|
55
38
|
export declare const drizzleDatabase: (connection: DrizzleSource, config?: Omit<DrizzleAdapterConfig, 'provider'>) => ReturnType<typeof drizzleAdapter>;
|
package/dist/guard.d.ts
CHANGED
|
@@ -10,20 +10,16 @@ import { AuthContext } from './context.js';
|
|
|
10
10
|
*/
|
|
11
11
|
export declare const rolesOf: (user: object) => readonly string[];
|
|
12
12
|
/**
|
|
13
|
-
* Authenticates every request
|
|
14
|
-
*
|
|
13
|
+
* Authenticates every request through better-auth's own session lookup, composing
|
|
14
|
+
* with the metadata `@dunx/http` carries:
|
|
15
15
|
*
|
|
16
|
-
* - `@Public()` - skipped outright
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* calls `auth.api.getSession({ headers: req.headers })` itself - one line, and it
|
|
21
|
-
* does not put a lookup on every public request in the app.
|
|
22
|
-
* - `@Roles('admin')` - a 403 unless the caller holds one of them.
|
|
16
|
+
* - `@Public()` - skipped outright, which is what makes it safe to install
|
|
17
|
+
* globally: better-auth's own endpoints are public. A public route adapting to
|
|
18
|
+
* an optional caller injects `Auth` and looks the session up itself.
|
|
19
|
+
* - `@Roles('admin')` - a 403 unless the caller holds one.
|
|
23
20
|
*
|
|
24
|
-
* Install it
|
|
25
|
-
*
|
|
26
|
-
* and leave the rest of the app open. `AuthModule` registers it either way.
|
|
21
|
+
* Install it in `HttpFactory.create(root, { middleware: [SessionGuard] })`, or
|
|
22
|
+
* scope it with `@UseGuards(SessionGuard)`. `AuthModule` registers it either way.
|
|
27
23
|
*/
|
|
28
24
|
export declare class SessionGuard implements Middleware {
|
|
29
25
|
private readonly auth;
|
package/dist/handler.d.ts
CHANGED
|
@@ -2,25 +2,14 @@ import { type Ctor } from '@dunx/core';
|
|
|
2
2
|
import { type Input, type RouteSchemas } from '@dunx/http';
|
|
3
3
|
/**
|
|
4
4
|
* better-auth's handler is a plain `(request: Request) => Promise<Response>`, so
|
|
5
|
-
* mounting it is five one-line routes
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* mounting it is five one-line routes. Every endpoint it and its plugins declare
|
|
6
|
+
* lives under one wildcard, which `Bun.serve` matches natively, so Bun is still
|
|
7
|
+
* the router. All five verbs, because a plugin may declare any of them.
|
|
8
8
|
*
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* The `Response` is returned untouched - `buildRoutes` passes one straight through,
|
|
14
|
-
* which is what keeps better-auth's `Set-Cookie` headers and redirects intact.
|
|
15
|
-
*
|
|
16
|
-
* `@Public()` at class scope, so all five inherit it - `mergeMeta` reads the class's
|
|
17
|
-
* record under the handler's. Without it a globally installed `SessionGuard` would
|
|
18
|
-
* demand a session from the sign-in endpoint, and no session could ever be created.
|
|
19
|
-
*
|
|
20
|
-
* `inject(Auth)` in a field rather than a constructor parameter, because a bare
|
|
21
|
-
* class in `controllers` is bound as a class provider and would then need
|
|
22
|
-
* `@dunx/transform`'s transform to have run. This way mounting works in an app that
|
|
23
|
-
* never added the preload.
|
|
9
|
+
* The `Response` is returned untouched, keeping `Set-Cookie` and redirects intact.
|
|
10
|
+
* `@Public()` at class scope, or a global `SessionGuard` would demand a session
|
|
11
|
+
* from the sign-in endpoint. `inject(Auth)` in a field rather than a constructor
|
|
12
|
+
* parameter, so mounting works without the transform preload.
|
|
24
13
|
*/
|
|
25
14
|
export declare class AuthHandler {
|
|
26
15
|
#private;
|
|
@@ -33,17 +22,12 @@ export declare class AuthHandler {
|
|
|
33
22
|
/**
|
|
34
23
|
* The controller `AuthModule` registers, prefixed with `AuthOptions.mountAt`.
|
|
35
24
|
*
|
|
36
|
-
* A subclass rather than `@Controller(...)` on {@link AuthHandler}
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* chain for the routes, and `metaOf` and `prefixOf` are plain lookups, so `@Public()`
|
|
41
|
-
* comes down from the base while the prefix stays own to the subclass.
|
|
25
|
+
* A subclass rather than `@Controller(...)` on {@link AuthHandler}: the prefix is
|
|
26
|
+
* only known once the module is configured, and mutating the shared class would
|
|
27
|
+
* make two configurations fight over one. The subclass inherits the routes and
|
|
28
|
+
* `@Public()` off the prototype chain while owning the prefix.
|
|
42
29
|
*
|
|
43
|
-
* `@ApiHidden()` because
|
|
44
|
-
*
|
|
45
|
-
* invalid entry tagged with this class's internal name - alongside the paths
|
|
46
|
-
* `betterAuthDocument` describes properly, which is where the auth surface should
|
|
47
|
-
* be read from.
|
|
30
|
+
* `@ApiHidden()` because `*` is not an OpenAPI path template;
|
|
31
|
+
* `betterAuthDocument` describes the auth surface properly.
|
|
48
32
|
*/
|
|
49
33
|
export declare const mountHandler: (mountAt: string) => Ctor<AuthHandler>;
|
package/dist/index.js
CHANGED
|
@@ -51,9 +51,7 @@ class AuthContext {
|
|
|
51
51
|
return this.#storage.run(principal, callback);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
-
Object.defineProperty(AuthContext, Symbol.for("dunx.deps"), {
|
|
55
|
-
value: () => [RequestContext]
|
|
56
|
-
});
|
|
54
|
+
Object.defineProperty(AuthContext, Symbol.for("dunx.deps"), { value: () => [RequestContext] });
|
|
57
55
|
// src/guard.ts
|
|
58
56
|
import {
|
|
59
57
|
HttpError as HttpError2,
|
|
@@ -98,9 +96,7 @@ class SessionGuard {
|
|
|
98
96
|
return this.context.run(principal, next);
|
|
99
97
|
}
|
|
100
98
|
}
|
|
101
|
-
Object.defineProperty(SessionGuard, Symbol.for("dunx.deps"), {
|
|
102
|
-
value: () => [Auth, AuthContext]
|
|
103
|
-
});
|
|
99
|
+
Object.defineProperty(SessionGuard, Symbol.for("dunx.deps"), { value: () => [Auth, AuthContext] });
|
|
104
100
|
// src/handler.ts
|
|
105
101
|
import { inject } from "@dunx/core";
|
|
106
102
|
import {
|
|
@@ -158,9 +154,7 @@ class AuthOptions {
|
|
|
158
154
|
this.options = withBunPassword({ ...init, basePath: this.basePath });
|
|
159
155
|
}
|
|
160
156
|
}
|
|
161
|
-
Object.defineProperty(AuthOptions, Symbol.for("dunx.deps"), {
|
|
162
|
-
value: () => [{ unresolved: "init: O" }, { unresolved: "mountAt?: string" }]
|
|
163
|
-
});
|
|
157
|
+
Object.defineProperty(AuthOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: O" }, { unresolved: "mountAt?: string" }] });
|
|
164
158
|
|
|
165
159
|
// src/handler.ts
|
|
166
160
|
var _dec = [
|
package/dist/module.d.ts
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import { type Deps, type DynamicModule, type AsyncModuleConfig } from '@dunx/core';
|
|
2
2
|
import { type BetterAuthOptions } from 'better-auth';
|
|
3
3
|
/**
|
|
4
|
-
* Binds
|
|
4
|
+
* Binds `AuthOptions`, `Auth` and `AuthContext`, plus a prefixed `AuthHandler`
|
|
5
|
+
* serving every better-auth endpoint under `basePath`.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - `AuthContext` - the authenticated caller, per request.
|
|
9
|
-
* - a prefixed `AuthHandler`, serving every better-auth endpoint under `basePath`.
|
|
10
|
-
*
|
|
11
|
-
* `SessionGuard` is registered as a provider rather than installed as global
|
|
12
|
-
* middleware, because whether it guards the whole app or one controller is the app's
|
|
13
|
-
* decision - pass it to `HttpFactory.create(root, { middleware: [SessionGuard] })`
|
|
14
|
-
* or to `@UseGuards(SessionGuard)`.
|
|
7
|
+
* `SessionGuard` is a provider rather than global middleware: whether it guards
|
|
8
|
+
* the whole app or one controller is the app's decision.
|
|
15
9
|
*/
|
|
16
10
|
export declare class AuthModule {
|
|
17
11
|
/**
|
|
@@ -33,9 +27,8 @@ export declare class AuthModule {
|
|
|
33
27
|
*/
|
|
34
28
|
static forRoot<const O extends BetterAuthOptions>(options: O, mountAt?: string): DynamicModule;
|
|
35
29
|
/**
|
|
36
|
-
* `forRoot` with the options behind a factory that may await and
|
|
37
|
-
*
|
|
38
|
-
* `ConfigService` rather than from module scope:
|
|
30
|
+
* `forRoot` with the options behind a factory that may await and inject, so the
|
|
31
|
+
* secret, base URL and database can come from `ConfigService`:
|
|
39
32
|
*
|
|
40
33
|
* ```ts
|
|
41
34
|
* AuthModule.forRootAsync({
|
|
@@ -49,13 +42,10 @@ export declare class AuthModule {
|
|
|
49
42
|
* });
|
|
50
43
|
* ```
|
|
51
44
|
*
|
|
52
|
-
* `mountAt` is a second,
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* the factory returns a non-default `basePath` is a boot error, because that
|
|
57
|
-
* combination could only ever have mounted the handler where better-auth is not
|
|
58
|
-
* looking.
|
|
45
|
+
* `mountAt` is a second, synchronous argument: the mount is a route in Bun's
|
|
46
|
+
* table, built before any factory has run. Only needed under a global prefix.
|
|
47
|
+
* Omitting it while the factory returns a non-default `basePath` is a boot
|
|
48
|
+
* error, since that would mount the handler where better-auth is not looking.
|
|
59
49
|
*/
|
|
60
50
|
static forRootAsync<const D extends Deps>(provider: AsyncModuleConfig<BetterAuthOptions, D>, mountAt?: string): DynamicModule;
|
|
61
51
|
}
|
package/dist/openapi.d.ts
CHANGED
|
@@ -38,16 +38,12 @@ export interface AuthDocumentOptions {
|
|
|
38
38
|
readonly tag?: string;
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
41
|
+
* better-auth's own endpoints, contributed to the app's OpenAPI document. It
|
|
42
|
+
* serves `<basePath>/*` from its own handler, so route discovery sees none of it
|
|
43
|
+
* and the document would omit the whole authentication surface.
|
|
42
44
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* describe an API missing its entire authentication surface. This asks the
|
|
46
|
-
* library for its schema and hands it over:
|
|
47
|
-
*
|
|
48
|
-
* **`forRootAsync`, not `forRoot`.** `forRoot` is evaluated while the module graph
|
|
49
|
-
* is being described, before there is a container, so there is nowhere for the
|
|
50
|
-
* `Auth` instance to come from. The async pair injects it:
|
|
45
|
+
* `forRootAsync`, not `forRoot`: the latter is evaluated while the module graph is
|
|
46
|
+
* described, before there is a container to take `Auth` from.
|
|
51
47
|
*
|
|
52
48
|
* ```ts
|
|
53
49
|
* OpenApiModule.forRootAsync({
|
|
@@ -61,16 +57,7 @@ export interface AuthDocumentOptions {
|
|
|
61
57
|
* });
|
|
62
58
|
* ```
|
|
63
59
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* **Better Auth only generates a schema when the `openAPI()` plugin is enabled.**
|
|
68
|
-
* Without it `generateOpenAPISchema` is absent and this contributes nothing rather
|
|
69
|
-
* than throwing, because a missing plugin should cost documentation and not boot.
|
|
70
|
-
* Pass `openAPI({ disableDefaultReference: true })` if you want the schema without
|
|
71
|
-
* Better Auth also mounting its own reference page next to the dunx one.
|
|
72
|
-
*
|
|
73
|
-
* Paths are rewritten to sit under `basePath`, since the library reports them
|
|
74
|
-
* relative to its own mount.
|
|
60
|
+
* A schema exists only with the `openAPI()` plugin enabled; without it this
|
|
61
|
+
* contributes nothing rather than throwing. Paths are rewritten under `basePath`.
|
|
75
62
|
*/
|
|
76
63
|
export declare const betterAuthDocument: (auth: OpenApiCapableAuth, options: AuthDocumentOptions) => () => Promise<AuthDocumentFragment>;
|
package/dist/password.d.ts
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* better-auth's `emailAndPassword.password`, backed by `Bun.password`.
|
|
2
|
+
* better-auth's `emailAndPassword.password`, backed by `Bun.password`. Applied by
|
|
3
|
+
* `AuthModule` when `emailAndPassword` is enabled and no hasher is given;
|
|
4
|
+
* better-auth's own default is JavaScript scrypt. Bun pre-hashes the input, so
|
|
5
|
+
* bcrypt's 72-byte cap is a non-issue.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* use Bun.
|
|
8
|
-
*
|
|
9
|
-
* Bun pre-hashes the input, so bcrypt's 72-byte cap is a non-issue even for a
|
|
10
|
-
* maximum-length multibyte password.
|
|
11
|
-
*
|
|
12
|
-
* `verify` swallows Bun's `UnsupportedAlgorithm` throw, so a hash produced by a
|
|
13
|
-
* *different* algorithm - a scrypt hash written before this was in place - is a
|
|
14
|
-
* clean authentication failure rather than a 500. Those users must reset their
|
|
15
|
-
* password to get a bcrypt hash; pass your own `password` implementation instead
|
|
16
|
-
* if you are migrating an existing user table and cannot.
|
|
7
|
+
* `verify` swallows Bun's `UnsupportedAlgorithm` throw, so a hash from a different
|
|
8
|
+
* algorithm is a clean authentication failure rather than a 500. Those users must
|
|
9
|
+
* reset; pass your own implementation if you are migrating a table and cannot.
|
|
17
10
|
*/
|
|
18
11
|
export declare const bunPassword: {
|
|
19
12
|
hash: (password: string) => Promise<string>;
|
package/dist/redis.d.ts
CHANGED
|
@@ -17,25 +17,17 @@ export interface RedisStore {
|
|
|
17
17
|
del(key: string): Promise<number>;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* better-auth's `secondaryStorage` over `Bun.RedisClient`, so sessions
|
|
21
|
-
*
|
|
22
|
-
* trip on every request.
|
|
20
|
+
* better-auth's `secondaryStorage` over `Bun.RedisClient`, so sessions and
|
|
21
|
+
* rate-limit counters cost no database round trip.
|
|
23
22
|
*
|
|
24
|
-
* All five methods
|
|
25
|
-
* `
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* falls back to read-then-delete for single-use credentials, which is a race, and to
|
|
29
|
-
* a non-atomic rate-limit counter.
|
|
23
|
+
* All five methods, not the three that are mandatory: `getAndDelete` and
|
|
24
|
+
* `increment` are optional because most clients cannot do them atomically, and
|
|
25
|
+
* `Bun.RedisClient` can through `GETDEL` and `INCR`. Without them better-auth
|
|
26
|
+
* falls back to a read-then-delete race and a non-atomic counter.
|
|
30
27
|
*
|
|
31
|
-
* `increment`'s TTL applies on creation only,
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* Redis being unreachable is deliberately **not** softened here. Bun's client
|
|
36
|
-
* connects lazily and queues, so a command against a down server rejects and
|
|
37
|
-
* better-auth's own error path is what should see it - a swallowed `null` from `get`
|
|
38
|
-
* would read as "no session" and sign every user out.
|
|
28
|
+
* `increment`'s TTL applies on creation only, so the window is fixed from the
|
|
29
|
+
* first hit. An unreachable Redis is not softened: a swallowed `null` would read
|
|
30
|
+
* as "no session" and sign every user out.
|
|
39
31
|
*/
|
|
40
32
|
export declare const redisStorage: (connection: RedisStore) => SecondaryStorage;
|
|
41
33
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dunx/auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Better Auth for dunx: its handler mounted on Bun.serve, a session guard reading @Public() and @Roles(), the caller in async context, and Bun.password hashing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"drizzle-orm": "^0.45.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@dunx/core": "^
|
|
62
|
-
"@dunx/http": "^
|
|
61
|
+
"@dunx/core": "^3.0.1",
|
|
62
|
+
"@dunx/http": "^3.0.1",
|
|
63
63
|
"@types/bun": ">=1.3.0",
|
|
64
64
|
"better-auth": "^1.6.25",
|
|
65
65
|
"drizzle-orm": "^0.45.2"
|