@dunx/auth 0.1.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.
- package/LICENSE +201 -0
- package/README.md +251 -0
- package/dist/auth.d.ts +42 -0
- package/dist/context.d.ts +45 -0
- package/dist/drizzle.d.ts +41 -0
- package/dist/drizzle.js +92 -0
- package/dist/drizzle.js.map +10 -0
- package/dist/errors.d.ts +8 -0
- package/dist/guard.d.ts +33 -0
- package/dist/handler.d.ts +43 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +373 -0
- package/dist/index.js.map +18 -0
- package/dist/module.d.ts +61 -0
- package/dist/options.d.ts +32 -0
- package/dist/password.d.ts +24 -0
- package/dist/redis.d.ts +41 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# @dunx/auth
|
|
2
|
+
|
|
3
|
+
[Better Auth](https://better-auth.com) for dunx. **This package is not an
|
|
4
|
+
authentication system** — better-auth is, and it is very good at it. This is the
|
|
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.
|
|
8
|
+
|
|
9
|
+
`better-auth` is a **required peer dependency**. Install it yourself and own its
|
|
10
|
+
version — dunx does not bundle it — but it is not optional, because this package
|
|
11
|
+
imports `betterAuth` as a value and cannot load without it. Marking it optional would
|
|
12
|
+
trade an install-time warning for a module-resolution crash.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun add @dunx/auth better-auth
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`drizzle-orm` **is** an optional peer, needed only by `@dunx/auth/drizzle` — which is
|
|
19
|
+
its own subpath precisely so that a Prisma, Kysely or MongoDB app never loads it.
|
|
20
|
+
`dist/index.js` contains no reference to drizzle, which is the test a peer has to
|
|
21
|
+
pass to be called optional.
|
|
22
|
+
|
|
23
|
+
There is no dunx sign-in flow, no dunx session table, no dunx password reset and no
|
|
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
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { AuthModule } from '@dunx/auth';
|
|
47
|
+
import { drizzleDatabase } from '@dunx/auth/drizzle';
|
|
48
|
+
import { Module } from '@dunx/core';
|
|
49
|
+
import { DbConnection } from '@dunx/infra/db';
|
|
50
|
+
import { admin, bearer } from 'better-auth/plugins';
|
|
51
|
+
|
|
52
|
+
@Module({
|
|
53
|
+
imports: [
|
|
54
|
+
AuthModule.forRootAsync({
|
|
55
|
+
useFactory: (config: AppConfigService, connection: DbConnection) => ({
|
|
56
|
+
secret: config.get('auth').secret,
|
|
57
|
+
baseURL: config.get('appUrl'),
|
|
58
|
+
database: drizzleDatabase(connection),
|
|
59
|
+
emailAndPassword: { enabled: true },
|
|
60
|
+
plugins: [admin(), bearer()],
|
|
61
|
+
}),
|
|
62
|
+
inject: [AppConfigService, DbConnection] as const,
|
|
63
|
+
}),
|
|
64
|
+
],
|
|
65
|
+
})
|
|
66
|
+
export class AccountsModule {}
|
|
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:
|
|
137
|
+
|
|
138
|
+
- **`@Public()`** — skipped outright. No session lookup, no rejection, no role check.
|
|
139
|
+
That is what makes the guard safe to install globally: `AuthHandler` is `@Public()`,
|
|
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. What does go there is `userId`, which is why 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
|
+
Rule 1's first half: 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
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Sessions, verification values and rate-limit counters then live in Redis instead of
|
|
226
|
+
costing a database round trip per request.
|
|
227
|
+
|
|
228
|
+
All five methods are implemented, not the three that are mandatory. `getAndDelete` and
|
|
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.
|
|
233
|
+
|
|
234
|
+
`redisStorage` takes a `RedisStore`, which is six methods restated rather than
|
|
235
|
+
imported — an `@dunx/infra/redis` `RedisConnection` satisfies it structurally, and so
|
|
236
|
+
does anything else shaped like `Bun.RedisClient`.
|
|
237
|
+
|
|
238
|
+
## What is bound
|
|
239
|
+
|
|
240
|
+
`AuthModule` binds four things and mounts one controller:
|
|
241
|
+
|
|
242
|
+
| Token | Resolves to |
|
|
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` |
|
|
248
|
+
|
|
249
|
+
Every one of them declares its own `inject` list, so none of it needs
|
|
250
|
+
`@dunx/transform`'s transform to have run — `@dunx/auth` works in an app with no
|
|
251
|
+
preload.
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Auth as Instance, BetterAuthOptions } from 'better-auth';
|
|
2
|
+
/**
|
|
3
|
+
* The injection token for the better-auth instance, and the whole of dunx's
|
|
4
|
+
* contract with the library.
|
|
5
|
+
*
|
|
6
|
+
* `betterAuth()` returns a plain object, so there is no class to use as a token.
|
|
7
|
+
* This is the same trick `Logger` and `RequestContext` use in `@dunx/core`: an
|
|
8
|
+
* abstract class whose members are **aliases of better-auth's own** — not
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
export declare abstract class Auth<O extends BetterAuthOptions = BetterAuthOptions> {
|
|
20
|
+
/**
|
|
21
|
+
* `abstract` stops TypeScript constructing this, but the container works on
|
|
22
|
+
* runtime values and every class self-binds — so `get(Auth)` with nothing bound
|
|
23
|
+
* would hand back a bare instance whose every member is `undefined`, and the
|
|
24
|
+
* first symptom would be `auth.handler is not a function` deep in a request.
|
|
25
|
+
*/
|
|
26
|
+
constructor();
|
|
27
|
+
/** better-auth's framework-agnostic handler. `AuthHandler` mounts it. */
|
|
28
|
+
abstract readonly handler: Instance<O>['handler'];
|
|
29
|
+
/** Every endpoint as a callable — `api.getSession`, `api.signUpEmail`, ... */
|
|
30
|
+
abstract readonly api: Instance<O>['api'];
|
|
31
|
+
/** The options `betterAuth()` was called with, dunx's defaults already applied. */
|
|
32
|
+
abstract readonly options: Instance<O>['options'];
|
|
33
|
+
abstract readonly $ERROR_CODES: Instance<O>['$ERROR_CODES'];
|
|
34
|
+
abstract readonly $context: Instance<O>['$context'];
|
|
35
|
+
abstract readonly $Infer: Instance<O>['$Infer'];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `{ session, user }` for an authenticated caller — better-auth's own inferred
|
|
39
|
+
* session type, so a plugin's extra user fields (the `admin` plugin's `role` and
|
|
40
|
+
* `banned`, say) are typed without dunx naming a single one of them.
|
|
41
|
+
*/
|
|
42
|
+
export type Principal<O extends BetterAuthOptions = BetterAuthOptions> = Instance<O>['$Infer']['Session'];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { RequestContext } from '@dunx/core';
|
|
2
|
+
import type { BetterAuthOptions } from 'better-auth';
|
|
3
|
+
import type { Principal } from './auth.js';
|
|
4
|
+
/**
|
|
5
|
+
* How the authenticated caller reaches a handler — and anything the handler calls,
|
|
6
|
+
* however deep.
|
|
7
|
+
*
|
|
8
|
+
* `AsyncLocalStorage`, for the same reason `@dunx/core`'s `RequestContext` is: it is
|
|
9
|
+
* a Node built-in Bun implements natively, and it is the only mechanism that gets a
|
|
10
|
+
* value from middleware to a service three constructor hops away without passing
|
|
11
|
+
* it. The alternatives were both worse — request-scoped DI was measured and rejected
|
|
12
|
+
* (docs/ARCHITECTURE.md), and hanging the principal off `req` reaches a route
|
|
13
|
+
* handler but nothing a route handler calls.
|
|
14
|
+
*
|
|
15
|
+
* It is a **second** store rather than a key in `RequestContext`. That store is the
|
|
16
|
+
* log record: every field in it is serialized into every line the request writes, so
|
|
17
|
+
* a session object there would be noise on each entry and a redaction hazard in the
|
|
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.
|
|
20
|
+
*/
|
|
21
|
+
export declare class AuthContext {
|
|
22
|
+
#private;
|
|
23
|
+
private readonly context;
|
|
24
|
+
constructor(context: RequestContext);
|
|
25
|
+
/**
|
|
26
|
+
* The caller, or `undefined` on an anonymous request. The type argument is the
|
|
27
|
+
* options object `AuthModule` was configured with, and is how a plugin's extra
|
|
28
|
+
* user fields become visible:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const principal = this.auth.current<typeof authOptions>();
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
current<O extends BetterAuthOptions = BetterAuthOptions>(): Principal<O> | undefined;
|
|
35
|
+
/** The caller, or a 401. For a handler behind `SessionGuard` that is not `@Public()`. */
|
|
36
|
+
require<O extends BetterAuthOptions = BetterAuthOptions>(): Principal<O>;
|
|
37
|
+
/**
|
|
38
|
+
* Runs `callback` with `principal` as the caller. `SessionGuard` is what calls
|
|
39
|
+
* this; a job or a socket handler that resolved a session itself can too.
|
|
40
|
+
*
|
|
41
|
+
* `userId` is written to `RequestContext` as well, which is what puts it on every
|
|
42
|
+
* log line the callback produces.
|
|
43
|
+
*/
|
|
44
|
+
run<T>(principal: Principal, callback: () => T): T;
|
|
45
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { drizzleAdapter, type DrizzleAdapterConfig } from 'better-auth/adapters/drizzle';
|
|
2
|
+
/**
|
|
3
|
+
* The two members of `@dunx/infra/db`'s `DbConnection` this reads.
|
|
4
|
+
*
|
|
5
|
+
* Restated structurally rather than imported, for the same reason `@dunx/http`
|
|
6
|
+
* restates Standard Schema: it keeps `@dunx/auth`'s dependency list at `@dunx/core`
|
|
7
|
+
* and `@dunx/http`, and it means a bare `drizzle({ client, schema })` handle works
|
|
8
|
+
* here too. An `@dunx/infra/db` connection satisfies it with no adapter in between —
|
|
9
|
+
* `dialect` is exactly that union and `db` is exactly `unknown`.
|
|
10
|
+
*/
|
|
11
|
+
export interface DrizzleSource {
|
|
12
|
+
readonly dialect: 'postgres' | 'mysql' | 'mariadb' | 'sqlite';
|
|
13
|
+
/** The drizzle handle — `BunSQLiteDatabase` or `BunSQLDatabase`. */
|
|
14
|
+
readonly db: unknown;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* better-auth's `database` option over a connection the app already opened. Nothing
|
|
18
|
+
* here connects: the point is that the app keeps **one** pool, one SQLite handle and
|
|
19
|
+
* one shutdown path, instead of better-auth opening a second.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* AuthModule.forRootAsync({
|
|
23
|
+
* useFactory: (connection: DbConnection) => ({
|
|
24
|
+
* database: drizzleDatabase(connection),
|
|
25
|
+
* }),
|
|
26
|
+
* inject: [DbConnection],
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* The `provider` comes from the connection's own dialect, so swapping `bun:sqlite`
|
|
31
|
+
* for `Bun.SQL` needs no edit at the call site. The schema does not have to be passed
|
|
32
|
+
* either — `@dunx/infra/db` builds its handle with `drizzle({ client, schema })` and
|
|
33
|
+
* the adapter reads `db._.fullSchema`, so the better-auth tables being in the app's
|
|
34
|
+
* schema object is the whole requirement.
|
|
35
|
+
*
|
|
36
|
+
* dunx ships **no** schema for those tables. They are better-auth's, they change with
|
|
37
|
+
* its plugins, and its own CLI generates them: `bunx @better-auth/cli generate`. A
|
|
38
|
+
* copy of them inside a framework is a copy that silently rots against the library
|
|
39
|
+
* that reads it.
|
|
40
|
+
*/
|
|
41
|
+
export declare const drizzleDatabase: (connection: DrizzleSource, config?: Omit<DrizzleAdapterConfig, 'provider'>) => ReturnType<typeof drizzleAdapter>;
|