@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 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>;