@opensaas/stack-auth 0.36.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +146 -0
  3. package/CLAUDE.md +153 -9
  4. package/README.md +18 -7
  5. package/dist/config/adopt-better-auth-tables.d.ts +47 -0
  6. package/dist/config/adopt-better-auth-tables.d.ts.map +1 -1
  7. package/dist/config/adopt-better-auth-tables.js +29 -1
  8. package/dist/config/adopt-better-auth-tables.js.map +1 -1
  9. package/dist/config/derive-auth-lists.d.ts +7 -5
  10. package/dist/config/derive-auth-lists.d.ts.map +1 -1
  11. package/dist/config/derive-auth-lists.js +33 -34
  12. package/dist/config/derive-auth-lists.js.map +1 -1
  13. package/dist/config/index.d.ts.map +1 -1
  14. package/dist/config/index.js +41 -11
  15. package/dist/config/index.js.map +1 -1
  16. package/dist/config/plugin.d.ts.map +1 -1
  17. package/dist/config/plugin.js +39 -27
  18. package/dist/config/plugin.js.map +1 -1
  19. package/dist/config/types.d.ts +143 -28
  20. package/dist/config/types.d.ts.map +1 -1
  21. package/dist/server/build-better-auth-options.test.d.ts +2 -0
  22. package/dist/server/build-better-auth-options.test.d.ts.map +1 -0
  23. package/dist/server/build-better-auth-options.test.js +29 -0
  24. package/dist/server/build-better-auth-options.test.js.map +1 -0
  25. package/dist/server/index.d.ts +112 -8
  26. package/dist/server/index.d.ts.map +1 -1
  27. package/dist/server/index.js +284 -96
  28. package/dist/server/index.js.map +1 -1
  29. package/dist/server/schema-converter.d.ts +3 -3
  30. package/dist/server/schema-converter.d.ts.map +1 -1
  31. package/package.json +5 -5
  32. package/src/config/adopt-better-auth-tables.ts +70 -1
  33. package/src/config/derive-auth-lists.ts +37 -38
  34. package/src/config/index.ts +47 -12
  35. package/src/config/plugin.ts +40 -28
  36. package/src/config/types.ts +144 -27
  37. package/src/server/build-better-auth-options.test.ts +59 -0
  38. package/src/server/index.ts +470 -106
  39. package/src/server/schema-converter.ts +3 -3
  40. package/tests/adopt-better-auth-tables.test.ts +99 -0
  41. package/tests/config.test.ts +66 -8
  42. package/tests/derive-auth-lists.test.ts +79 -5
  43. package/tests/generated-fk-shape.test.ts +65 -0
  44. package/tests/plugin-derived-keys.test.ts +48 -0
  45. package/tests/server.test.ts +723 -0
  46. package/tsconfig.tsbuildinfo +1 -1
  47. package/vitest.config.ts +7 -1
@@ -1,4 +1,4 @@
1
1
 
2
- > @opensaas/stack-auth@0.36.0 build /home/runner/work/stack/stack/packages/auth
2
+ > @opensaas/stack-auth@0.38.0 build /home/runner/work/stack/stack/packages/auth
3
3
  > tsc
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,151 @@
1
1
  # @opensaas/stack-auth
2
2
 
3
+ ## 0.38.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#888](https://github.com/OpenSaasAU/stack/pull/888) [`8183827`](https://github.com/OpenSaasAU/stack/commit/8183827ec65d6cfd7153028f84057ab65dfdc7dd) Thanks [@borisno2](https://github.com/borisno2)! - `buildBetterAuthOptions()` and `createAuth()` now accept an optional third argument — your app's `betterAuthPlugins` array, the same array passed to `authPlugin({ betterAuthPlugins })` — so the returned options/`Auth` type carries the literal plugin tuple instead of the widened `BetterAuthOptions`/`Auth<BetterAuthOptions>`. Without this, `betterAuth()` constructed from the widened return loses plugin-derived `auth.api.*` endpoints (e.g. `emailOTP()`'s `signInEmailOTP`) and a `customSession()` plugin's replaced session shape.
8
+
9
+ ```typescript
10
+ export const appBetterAuthPlugins = [emailOTP({ sendVerificationOTP })] // same array passed to authPlugin({ betterAuthPlugins })
11
+
12
+ export const auth = betterAuth({
13
+ ...(await buildBetterAuthOptions(config, rawOpensaasContext, appBetterAuthPlugins)),
14
+ })
15
+ // auth.api.signInEmailOTP is now typed, and auth.api.getSession() returns your customSession() shape.
16
+ ```
17
+
18
+ The supplied tuple is for typing only — the plugin array used at runtime is always the one resolved from `authPlugin({ betterAuthPlugins })`. Passing a tuple that isn't the same plugin instances in the same order throws, naming the mismatch, so the two can't silently drift apart. Calling either function with no third argument is unchanged — same widened return type, same runtime options, fully backwards compatible.
19
+
20
+ Also, `AuthConfig`/`NormalizedAuthConfig`'s `betterAuthPlugins` field is now typed as better-auth's own `BetterAuthPlugin[]` instead of `any[]`.
21
+
22
+ - [#889](https://github.com/OpenSaasAU/stack/pull/889) [`b9b9357`](https://github.com/OpenSaasAU/stack/commit/b9b935719774b01a81cfd2082387b76806c1a484) Thanks [@borisno2](https://github.com/borisno2)! - Fix `getSessionFromAuth` to project `sessionFields` from the _resolved_ better-auth session instead of only its `user` sub-object. A `customSession` plugin's replaced shape with no `user` key is now correctly treated as a signed-in session (never misreported as anonymous), and a session-only field (e.g. the admin plugin's `impersonatedBy`) is now resolvable. Errors from the underlying session lookup now propagate instead of silently becoming `null`, and a `sessionFields` entry that can't be resolved is omitted and logs a warning (once per field, per process) instead of vanishing silently.
23
+
24
+ The scaffolded `getSession()` — the CLI feature generator's `lib/auth.ts` template, and `examples/starter-auth`/`examples/auth-demo` — now call this single shared helper, reading `sessionFields` from the resolved config at runtime instead of baking a field list in at generation time. `examples/auth-demo`'s `getSession()` also now correctly returns `null` for an anonymous visitor (previously returned a truthy object of `undefined` values).
25
+
26
+ ```typescript
27
+ authPlugin({ sessionFields: ['userId', 'email', 'name', 'role'] })
28
+ ```
29
+
30
+ ```typescript
31
+ // lib/auth.ts
32
+ export async function getSession() {
33
+ const resolvedConfig = await config
34
+ const authConfig = resolvedConfig._pluginData?.auth as NormalizedAuthConfig | undefined
35
+ const sessionFields = authConfig?.sessionFields ?? ['userId', 'email', 'name']
36
+ return getSessionFromAuth(auth, sessionFields, await headers())
37
+ }
38
+ ```
39
+
40
+ ## 0.37.0
41
+
42
+ ### Minor Changes
43
+
44
+ - [#872](https://github.com/OpenSaasAU/stack/pull/872) [`17acf04`](https://github.com/OpenSaasAU/stack/commit/17acf046b494da184c2b77434a7b4d3400ca32f2) Thanks [@borisno2](https://github.com/borisno2)! - `createAuth()` now forwards `AuthConfig` options it previously normalized but silently dropped: `emailAndPassword.minPasswordLength`, `passwordReset.enabled`/`tokenExpiration` (wired to better-auth's `sendResetPassword`), and `emailVerification.enabled`/`sendOnSignUp`/`tokenExpiration` (wired to `sendVerificationEmail`).
45
+
46
+ The stack does not wrap these email callbacks in any way — `emailAndPassword.sendResetPassword` and `emailVerification.sendVerificationEmail` are better-auth's own option shape, forwarded straight through, so an app configures them exactly as it would when calling `betterAuth()` directly:
47
+
48
+ ```typescript
49
+ authPlugin({
50
+ emailAndPassword: {
51
+ enabled: true,
52
+ sendResetPassword: async ({ user, url }) => {
53
+ await resend.emails.send({
54
+ to: user.email,
55
+ subject: 'Reset your password',
56
+ html: `<a href="${url}">Reset your password</a>`,
57
+ })
58
+ },
59
+ },
60
+ emailVerification: {
61
+ enabled: true,
62
+ sendVerificationEmail: async ({ user, url }) => {
63
+ await resend.emails.send({
64
+ to: user.email,
65
+ subject: 'Verify your email',
66
+ html: `<a href="${url}">Verify your email</a>`,
67
+ })
68
+ },
69
+ },
70
+ })
71
+ ```
72
+
73
+ If not provided, reset/verification emails are logged to console instead of sent — apps relying on the previous no-op behavior (verification/reset emails silently not sending) will start sending real emails once `emailVerification`/`passwordReset` are enabled and these callbacks are configured.
74
+
75
+ Two related fixes, both changing existing behavior:
76
+
77
+ - `session.updateAge` is retyped from `boolean` to `number | false` — the number of seconds between session refreshes, passed straight through to better-auth's own `session.updateAge` instead of being computed as `expiresIn / 10`. The default changes from `true` to `86400` (1 day), matching better-auth's own default. Update any `updateAge: true` config to a duration in seconds (e.g. `86400`). `updateAge: false` now correctly maps to better-auth's `disableSessionRefresh: true` (previously it mapped to `updateAge: 0`, which better-auth treats as "refresh on every request" — the opposite of disabling refresh).
78
+ - `getSessionFromAuth(auth, sessionFields, headers)` gains a required third `headers: Headers` parameter. Previously it always called `auth.api.getSession({ headers: new Headers() })`, an empty header set that could never resolve a session cookie, so the function always returned `null`. Callers must now pass the request's real headers (e.g. Next.js `await headers()`).
79
+
80
+ Setting `emailAndPassword.requireConfirmation` (while `emailAndPassword.enabled` is true) now logs a `console.warn` — it has no better-auth server-side equivalent (it's a UI-only "confirm password" concern). Pass `requirePasswordConfirmation` directly to `<SignUpForm>`/`<ResetPasswordForm>` instead. Similarly, `passwordReset.enabled` now warns if `emailAndPassword.enabled` is false, since password reset has no effect without a password-based account.
81
+
82
+ - [#871](https://github.com/OpenSaasAU/stack/pull/871) [`06375ca`](https://github.com/OpenSaasAU/stack/commit/06375cad571677e92bfe84c35ff55240f3546a1f) Thanks [@{](https://github.com/{)! - Add a per-model `tableName` option, independent of `modelName`, so a renamed Auth list key can still adopt a differently-named live table — most commonly better-auth's own default lowercase table names (`user`, `session`, `account`, `verification`).
83
+
84
+ ```typescript
85
+ authPlugin({
86
+ modelName: 'AuthUser', tableName: 'user' },
87
+ session: { modelName: 'AuthSession', tableName: 'session' },
88
+ })
89
+ ```
90
+
91
+ `adoptBetterAuthTables()` gains matching `useBetterAuthTableNames` and `tableNames` options:
92
+
93
+ ```typescript
94
+ adoptBetterAuthTables({ useBetterAuthTableNames: true })
95
+ // or explicitly:
96
+ adoptBetterAuthTables({ tableNames: { user: 'user', session: 'session' } })
97
+ ```
98
+
99
+ With no `tableName` set, behaviour is unchanged: the table name still follows `modelName` when it differs from the better-auth default, otherwise no `@@map` is emitted.
100
+
101
+ - [#874](https://github.com/OpenSaasAU/stack/pull/874) [`7ef9dbc`](https://github.com/OpenSaasAU/stack/commit/7ef9dbc2f94cc4e7ab831ecafb3ef65159a3c55e) Thanks [@borisno2](https://github.com/borisno2)! - Add a `betterAuthOptions` escape hatch on `AuthConfig` for better-auth options the stack doesn't model, plus an exported `buildBetterAuthOptions()` builder for apps that still need to hand-wire their own `betterAuth()` instance.
102
+
103
+ `betterAuthOptions` is deep-merged onto the options `createAuth()` builds, applied last — a plain-object value merges recursively alongside sibling keys the stack already set (e.g. `session: { cookieCache }` doesn't clobber `session.expiresIn`), and wins on any genuine key collision:
104
+
105
+ ```typescript
106
+ authPlugin({
107
+ betterAuthOptions: {
108
+ databaseHooks: { user: { create: { after: syncDomainUser } } },
109
+ session: { cookieCache: { enabled: true, maxAge: 300 } },
110
+ verification: { storeIdentifier: 'hashed' },
111
+ baseURL: process.env.BETTER_AUTH_URL,
112
+ },
113
+ })
114
+ ```
115
+
116
+ `database`, `plugins`, and `additionalFields` under `user`/`session`/`account`/`verification` are rejected — they already have dedicated seams (`db` config, `betterAuthPlugins`), or have schema consequences a passthrough can't also apply to the generated Prisma schema.
117
+
118
+ `buildBetterAuthOptions(config, context)` returns the exact same options object `createAuth()` uses, for apps that need a resolved `betterAuth()` instance rather than `createAuth()`'s lazy proxy:
119
+
120
+ ```typescript
121
+ import { betterAuth } from 'better-auth'
122
+ import { buildBetterAuthOptions } from '@opensaas/stack-auth/server'
123
+
124
+ export const auth = betterAuth({
125
+ ...(await buildBetterAuthOptions(config, rawOpensaasContext)),
126
+ databaseHooks: { user: { create: { after: syncDomainUser } } },
127
+ })
128
+ ```
129
+
130
+ - [#870](https://github.com/OpenSaasAU/stack/pull/870) [`7b6189f`](https://github.com/OpenSaasAU/stack/commit/7b6189fa60119a45082ba62dd71d915d93de529c) Thanks [@relationship({](https://github.com/relationship({)! - A relationship field's foreign key can now be declared non-nullable via `db.isNullable: false` — the generated FK column and its relation field lose their `?` together. Omitting the option leaves every existing relationship unchanged (still nullable by default).
131
+
132
+ ```typescript
133
+
134
+ ref: 'User.sessions',
135
+ db: { isNullable: false },
136
+ })
137
+ // Generates: userId String (was String?)
138
+ // user User @relation(...) (was User?)
139
+ ```
140
+
141
+ `@opensaas/stack-auth`'s derived Auth lists now use this to match better-auth's own Prisma schema: `Session.expiresAt`, `Verification.expiresAt`, and the `Session.user`/`Account.user` foreign keys generate as required instead of nullable.
142
+
143
+ **Migration note:** this changes the generated schema for existing greenfield apps. Running `opensaas generate` followed by `prisma db push`/`prisma migrate dev` will produce a migration that adds `NOT NULL` to `Session.expiresAt`, `Verification.expiresAt`, `Session.userId`, and `Account.userId`. Since better-auth's own adapter always writes these columns, no existing row should violate the new constraint — but back up production data before applying, as with any schema migration.
144
+
145
+ ### Patch Changes
146
+
147
+ - [#867](https://github.com/OpenSaasAU/stack/pull/867) [`43b4d17`](https://github.com/OpenSaasAU/stack/commit/43b4d1738340f05b1cf8bec3315927b3004816dd) Thanks [@borisno2](https://github.com/borisno2)! - Fix a better-auth plugin's schema extension of a base model (`user`/`session`/`account`/`verification`) silently dropping the derived Auth list's `db` (`map`/`schema`/`timestamps`) and `access` config.
148
+
3
149
  ## 0.36.0
4
150
 
5
151
  ## 0.35.0
package/CLAUDE.md CHANGED
@@ -28,7 +28,8 @@ Auto-generated lists:
28
28
 
29
29
  ### Server (`src/server/index.ts`)
30
30
 
31
- - `createAuth(config, rawContext?)` - Creates Better-auth instance with MCP plugin support
31
+ - `createAuth(config, rawContext?, betterAuthPlugins?)` - Creates Better-auth instance with MCP plugin support
32
+ - `buildBetterAuthOptions(config, rawContext?, betterAuthPlugins?)` - Returns the same `BetterAuthOptions` `createAuth()` builds, without constructing an instance — for apps that need to hand-wire their own `betterAuth()`. The optional third argument (the app's own `betterAuthPlugins` array) makes the return type carry that literal plugin tuple instead of the widened array type — see "Typed `auth.api.*` reads" below.
32
33
  - Returns `{ handler, signIn, signOut, ... }` - Better-auth methods
33
34
 
34
35
  ### Client (`src/client/index.ts`)
@@ -71,13 +72,16 @@ developer writes — not hardcoded. The pure derivation lives in
71
72
  `src/config/derive-auth-lists.ts` (`deriveAuthLists`), which `getAuthLists`
72
73
  and the plugin's add-vs-extend logic consume:
73
74
 
74
- - per-model `modelName` → list key + table `@@map`
75
+ - per-model `modelName` → list key (and Prisma model name)
76
+ - per-model `tableName` → table `@@map`, **independent of `modelName`**
77
+ (defaults to `modelName` when it differs from the better-auth default,
78
+ otherwise unset — i.e. unchanged output when `tableName` isn't set)
75
79
  - per-model `fields` (better-auth field → column) → field-level `@map`
76
80
  - the `userId` column override → the `user` relationship foreign-key `@map`
77
81
  - relationship refs between the Auth lists follow the derived keys
78
82
  (e.g. `Session.user → AuthUser.sessions`)
79
83
 
80
- With no `modelName`/`fields` overrides the output is unchanged
84
+ With no `modelName`/`tableName`/`fields` overrides the output is unchanged
81
85
  (`User`/`Session`/`Account`/`Verification`, original field shapes, no `@@map`).
82
86
 
83
87
  ```typescript
@@ -89,6 +93,15 @@ authPlugin({
89
93
  // Adds AuthUser/AuthSession/... and leaves an app's own `User` untouched.
90
94
  ```
91
95
 
96
+ ```typescript
97
+ // modelName sets the list key; tableName independently pins the live table —
98
+ // e.g. a prefixed list key adopting better-auth's own default lowercase table.
99
+ authPlugin({
100
+ user: { modelName: 'AuthUser', tableName: 'user' },
101
+ session: { modelName: 'AuthSession', tableName: 'session' },
102
+ })
103
+ ```
104
+
92
105
  Because the plugin only ever adds/extends its **derived** keys, an app's own
93
106
  domain `User` (a different model from the better-auth user) is never extended
94
107
  or overwritten when the user model is renamed. The runtime `getUser`/
@@ -164,10 +177,10 @@ How it wires up (Postgres multi-schema):
164
177
 
165
178
  `adoptBetterAuthTables()` (`src/config/adopt-better-auth-tables.ts`) is a thin
166
179
  recipe that returns the `AuthConfig` adoption knobs — the plugin-level `schema`
167
- plus a per-model `modelName` (and optional column `fields` maps) — preset to the
168
- conventions of a standard separate-schema better-auth install. It ties together
169
- the keys/field derivation and schema placement so a migrator doesn't rebuild the
170
- config by hand. Spread it into `authPlugin`:
180
+ plus a per-model `modelName` (and optional column `fields`/`tableName` maps) —
181
+ preset to the conventions of a standard separate-schema better-auth install. It
182
+ ties together the keys/field derivation and schema placement so a migrator
183
+ doesn't rebuild the config by hand. Spread it into `authPlugin`:
171
184
 
172
185
  ```typescript
173
186
  import { authPlugin, adoptBetterAuthTables } from '@opensaas/stack-auth'
@@ -176,7 +189,23 @@ authPlugin({
176
189
  ...adoptBetterAuthTables(), // schema: 'auth', AuthUser/AuthSession/AuthAccount/AuthVerification
177
190
  emailAndPassword: { enabled: true },
178
191
  })
179
- // Options: adoptBetterAuthTables({ schema, modelNamePrefix, fields })
192
+ // Options: adoptBetterAuthTables({ schema, modelNamePrefix, fields, useBetterAuthTableNames, tableNames })
193
+ ```
194
+
195
+ The most common adoption shape is a project that ran better-auth **before**
196
+ Stack, so its live tables are still better-auth's own default lowercase names
197
+ (`user`/`session`/`account`/`verification`) even though the derived list keys
198
+ need an `Auth` prefix to avoid colliding with the app's own domain `User`.
199
+ `useBetterAuthTableNames: true` sets every model's `tableName` to that
200
+ default; the per-model `tableNames` map is the escape hatch for a mix (it
201
+ wins over `useBetterAuthTableNames` for any model it names):
202
+
203
+ ```typescript
204
+ authPlugin({
205
+ ...adoptBetterAuthTables({ useBetterAuthTableNames: true }),
206
+ // → AuthUser/AuthSession/AuthAccount/AuthVerification list keys,
207
+ // @@map("user")/@@map("session")/@@map("account")/@@map("verification")
208
+ })
180
209
  ```
181
210
 
182
211
  It is pure config (no side effects): everything it sets can also be written
@@ -206,7 +235,8 @@ const context = createContext(config, prisma, session)
206
235
 
207
236
  ### Session Fields Configuration
208
237
 
209
- Control which User fields appear in session:
238
+ `sessionFields` describes a **flattened projection** of the resolved better-auth session, not
239
+ the session's own shape:
210
240
 
211
241
  ```typescript
212
242
  authPlugin({ sessionFields: ['userId', 'email', 'name', 'role'] })
@@ -218,6 +248,15 @@ access: {
218
248
  }
219
249
  ```
220
250
 
251
+ `getSessionFromAuth()` (`@opensaas/stack-auth/server`) is the single implementation of this
252
+ projection — the scaffolded `getSession()` calls it with `sessionFields` read from the resolved
253
+ config at runtime. Each name resolves against a fixed precedence (a top-level key on the
254
+ resolved session, then `user`, then `session`), with `userId` special-cased to the user's `id`.
255
+ A `customSession` better-auth plugin fully replaces the resolved shape and can nest fields
256
+ anywhere; reconciling that against `sessionFields` is the app's job — an unresolvable name is
257
+ omitted and warns once (per field, per process) rather than silently becoming `undefined`. See
258
+ the `sessionFields` reference (`docs/content/reference/auth.md`) for the full contract.
259
+
221
260
  ### Session Type Safety
222
261
 
223
262
  To get autocomplete and type safety for session fields, use module augmentation:
@@ -294,6 +333,111 @@ authPlugin({
294
333
  })
295
334
  ```
296
335
 
336
+ ### Escape hatch for unmodelled better-auth options (`betterAuthOptions`)
337
+
338
+ `AuthConfig` models a deliberately closed set of better-auth options. For
339
+ anything the stack doesn't model — database hooks, `session.cookieCache`,
340
+ `baseURL`, `verification.storeIdentifier`, and so on — pass it through
341
+ `betterAuthOptions`, typed as better-auth's own `BetterAuthOptions` so it
342
+ tracks better-auth's surface without the stack re-declaring it:
343
+
344
+ ```typescript
345
+ authPlugin({
346
+ betterAuthOptions: {
347
+ databaseHooks: { user: { create: { after: syncDomainUser } } },
348
+ session: { cookieCache: { enabled: true, maxAge: 300 } },
349
+ verification: { storeIdentifier: 'hashed' },
350
+ baseURL: process.env.BETTER_AUTH_URL,
351
+ },
352
+ })
353
+ ```
354
+
355
+ `createAuth()` (`src/server/index.ts`) deep-merges `betterAuthOptions` onto
356
+ the options it builds from the rest of `AuthConfig`, applied **last**:
357
+ plain-object values merge recursively per key (so `session: { cookieCache }`
358
+ lands alongside the stack's own `session.expiresIn`/`updateAge` instead of
359
+ replacing the whole `session` block), arrays and other value types replace
360
+ outright, and `betterAuthOptions` wins on any genuine key collision. The
361
+ merge and the option-building it merges onto both live in
362
+ `buildBetterAuthOptions()`, the single place `createAuth()` and the exported
363
+ builder share — they cannot drift from each other because `createAuth()`
364
+ calls it directly rather than reimplementing it.
365
+
366
+ `database` and `plugins` are rejected outright (`assertNoUnsupportedPassthroughKeys`
367
+ in `src/server/index.ts`): they already have dedicated seams (`db` in the
368
+ stack config, and `betterAuthPlugins` respectively), and accepting them here
369
+ would create two unranked ways to set the same thing — worse for `plugins`,
370
+ since the stack must append `nextCookies()` last (see "Auth forms submit
371
+ through server actions" below). `additionalFields` under `user`/`session`/
372
+ `account`/`verification` is rejected too — it adds columns with no
373
+ corresponding change to the generated Prisma schema, which is exactly the
374
+ silent-divergence failure mode this passthrough exists to avoid elsewhere.
375
+ Add fields to the derived list instead: `extendUserList` for the user model,
376
+ or declare the list yourself in your own `lists` config for the others (the
377
+ auth plugin's `addList`-vs-`extendList` logic — see "Deriving Auth lists from
378
+ better-auth config" above — merges in field additions for any list matching
379
+ one of its derived keys).
380
+
381
+ ### Hand-wiring `betterAuth()` from the stack config (`buildBetterAuthOptions`)
382
+
383
+ An app that needs a resolved `betterAuth()` instance at module-init time
384
+ (rather than `createAuth()`'s lazy proxy) can derive its options from the
385
+ stack config instead of duplicating them:
386
+
387
+ ```typescript
388
+ import { betterAuth } from 'better-auth'
389
+ import { buildBetterAuthOptions } from '@opensaas/stack-auth/server'
390
+
391
+ export const auth = betterAuth({
392
+ ...(await buildBetterAuthOptions(config, rawOpensaasContext)),
393
+ databaseHooks: { user: { create: { after: syncDomainUser } } }, // not yet in betterAuthOptions
394
+ })
395
+ ```
396
+
397
+ This is the same async-resolve-then-construct step `createAuth()` performs
398
+ internally, exported standalone — see ADR-0014 and root `CLAUDE.md`'s
399
+ "Getting the ORM client outside a request" for why `createAuth()` itself
400
+ can't be synchronous. It gives an incremental path onto `createAuth()`: adopt
401
+ the builder first, then fold options into `betterAuthOptions` above as the
402
+ stack grows first-class config for them.
403
+
404
+ **Typed `auth.api.*` reads.** Called with just `(config, context)`, both
405
+ `buildBetterAuthOptions()` and `createAuth()` return the widened
406
+ `BetterAuthOptions` / `Auth<BetterAuthOptions>` — better-auth infers plugin
407
+ endpoints and a `customSession()`'s replaced session shape from the _literal_
408
+ options type, so the widened form erases them (an `emailOTP()` plugin loses
409
+ `auth.api.signInEmailOTP`; `auth.api.getSession()` falls back to `{ user,
410
+ session }` instead of a `customSession()` shape). Both functions take the
411
+ app's `betterAuthPlugins` array — the exact same array passed to
412
+ `authPlugin({ betterAuthPlugins })` — as an optional third argument, and their
413
+ return type then carries that literal tuple (plus the `nextCookies()` the
414
+ stack always appends last) instead of the widened array type:
415
+
416
+ ```typescript
417
+ export const appBetterAuthPlugins = [emailOTP({ sendVerificationOTP })] // same array passed to authPlugin({ betterAuthPlugins })
418
+
419
+ export const auth = betterAuth({
420
+ ...(await buildBetterAuthOptions(config, rawOpensaasContext, appBetterAuthPlugins)),
421
+ })
422
+ // auth.api.signInEmailOTP is now typed.
423
+ ```
424
+
425
+ The supplied tuple is for typing only — the plugin array used at runtime is
426
+ always the one resolved from `authPlugin({ betterAuthPlugins })` — so both
427
+ functions verify the supplied tuple is the same plugin instances in the same
428
+ order as the resolved array, throwing a prefixed error naming the mismatch if
429
+ not (`assertPluginTupleMatchesResolved` in `src/server/index.ts`). This is
430
+ what closes the drift hole a hand-rolled re-pass of the plugin array would
431
+ otherwise open.
432
+
433
+ `createAuth()`'s lazy `Proxy` does not behave identically to a real `Auth`
434
+ instance for every property regardless of which form you use — every access,
435
+ including a non-function property, is surfaced through an `async` wrapper (so
436
+ `auth.options` reads back as a `Promise`, not the plain object a real
437
+ instance returns synchronously). Reach for `buildBetterAuthOptions()` plus
438
+ `betterAuth()` instead when the app reads `auth.api.*` in typed code — it
439
+ constructs a real instance and does not have this gap.
440
+
297
441
  ## Integration Points
298
442
 
299
443
  ### With @opensaas/stack-core
package/README.md CHANGED
@@ -164,7 +164,15 @@ authPlugin({
164
164
  emailAndPassword: {
165
165
  enabled: true,
166
166
  minPasswordLength: 8,
167
- requireConfirmation: true,
167
+ requireConfirmation: true, // UI-only — pass to <SignUpForm>/<ResetPasswordForm> directly
168
+ // Passed straight through to better-auth's own `emailAndPassword.sendResetPassword`
169
+ sendResetPassword: async ({ user, url }) => {
170
+ await yourEmailService.send({
171
+ to: user.email,
172
+ subject: 'Reset your password',
173
+ html: `<a href="${url}">Reset your password</a>`,
174
+ })
175
+ },
168
176
  },
169
177
 
170
178
  // Email verification
@@ -172,6 +180,14 @@ authPlugin({
172
180
  enabled: true,
173
181
  sendOnSignUp: true,
174
182
  tokenExpiration: 86400, // 24 hours in seconds
183
+ // Passed straight through to better-auth's own `emailVerification.sendVerificationEmail`
184
+ sendVerificationEmail: async ({ user, url }) => {
185
+ await yourEmailService.send({
186
+ to: user.email,
187
+ subject: 'Verify your email',
188
+ html: `<a href="${url}">Verify your email</a>`,
189
+ })
190
+ },
175
191
  },
176
192
 
177
193
  // Password reset
@@ -195,7 +211,7 @@ authPlugin({
195
211
  // Session configuration
196
212
  session: {
197
213
  expiresIn: 604800, // 7 days in seconds
198
- updateAge: true, // Refresh session on each request
214
+ updateAge: 86400, // seconds between session refreshes; set `false` to disable
199
215
  },
200
216
 
201
217
  // Fields to include in session object
@@ -208,11 +224,6 @@ authPlugin({
208
224
  company: text(),
209
225
  },
210
226
  },
211
-
212
- // Custom email sending function
213
- sendEmail: async ({ to, subject, html }) => {
214
- await yourEmailService.send({ to, subject, html })
215
- },
216
227
  })
217
228
  ```
218
229
 
@@ -28,6 +28,22 @@
28
28
  * auth migration. The recipe never touches the application's own domain `User`:
29
29
  * its model names are `Auth`-prefixed by default and the plugin only ever
30
30
  * adds/extends its *derived* keys.
31
+ *
32
+ * The single most common adoption shape is a project that ran better-auth
33
+ * *before* adding Stack: its live tables are still better-auth's own default
34
+ * lowercase names (`user`/`session`/`account`/`verification`), even though the
35
+ * derived list keys need the `Auth` prefix to avoid colliding with the app's
36
+ * own domain `User`. Pass `useBetterAuthTableNames: true` to point every
37
+ * model's physical table at that default while keeping the prefixed list keys
38
+ * (or `tableNames` for an explicit per-model override):
39
+ *
40
+ * ```typescript
41
+ * authPlugin({
42
+ * ...adoptBetterAuthTables({ useBetterAuthTableNames: true }),
43
+ * // AuthUser/AuthSession/AuthAccount/AuthVerification list keys,
44
+ * // @@map("user")/@@map("session")/@@map("account")/@@map("verification")
45
+ * })
46
+ * ```
31
47
  */
32
48
  import type { AuthConfig } from './types.js';
33
49
  /**
@@ -85,6 +101,37 @@ export type AdoptBetterAuthTablesOptions = {
85
101
  account?: Record<string, string>;
86
102
  verification?: Record<string, string>;
87
103
  };
104
+ /**
105
+ * Set every model's physical table name to better-auth's own default
106
+ * lowercase table name (`user`, `session`, `account`, `verification`) —
107
+ * independent of the prefixed list key/`modelName`.
108
+ *
109
+ * This is the single most common adoption shape: a project that ran
110
+ * better-auth before adding the stack has exactly these tables, and the
111
+ * default `modelNamePrefix: 'Auth'` alone would otherwise pin the table
112
+ * name to the prefixed model name (`AuthUser`, ...), which `prisma migrate
113
+ * diff` reads as a rename against the live `user` table.
114
+ *
115
+ * Ignored for a model with an explicit entry in {@link tableNames}.
116
+ *
117
+ * @default false
118
+ */
119
+ useBetterAuthTableNames?: boolean;
120
+ /**
121
+ * Per-model explicit table name overrides, keyed by model. Takes
122
+ * precedence over `useBetterAuthTableNames` for that model.
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * adoptBetterAuthTables({ tableNames: { user: 'users' } })
127
+ * ```
128
+ */
129
+ tableNames?: {
130
+ user?: string;
131
+ session?: string;
132
+ account?: string;
133
+ verification?: string;
134
+ };
88
135
  };
89
136
  /**
90
137
  * The adoption-relevant slice of {@link AuthConfig}: the plugin-level `schema`
@@ -1 +1 @@
1
- {"version":3,"file":"adopt-better-auth-tables.d.ts","sourceRoot":"","sources":["../../src/config/adopt-better-auth-tables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,YAAY,CAAA;AAE7D;;;;;;GAMG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACzC;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE;QACP,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAChC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KACtC,CAAA;CACF,CAAA;AAUD;;;;GAIG;AACH,MAAM,MAAM,2BAA2B,GAAG,IAAI,CAC5C,UAAU,EACV,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,CAC3D,CAAA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,2BAA2B,CAqB7B"}
1
+ {"version":3,"file":"adopt-better-auth-tables.d.ts","sourceRoot":"","sources":["../../src/config/adopt-better-auth-tables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,YAAY,CAAA;AAE7D;;;;;;GAMG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACzC;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE;QACP,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAChC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KACtC,CAAA;IAED;;;;;;;;;;;;;;OAcG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAA;IAEjC;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAA;CACF,CAAA;AAkBD;;;;GAIG;AACH,MAAM,MAAM,2BAA2B,GAAG,IAAI,CAC5C,UAAU,EACV,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,CAC3D,CAAA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,2BAA2B,CAiC7B"}
@@ -28,6 +28,22 @@
28
28
  * auth migration. The recipe never touches the application's own domain `User`:
29
29
  * its model names are `Auth`-prefixed by default and the plugin only ever
30
30
  * adds/extends its *derived* keys.
31
+ *
32
+ * The single most common adoption shape is a project that ran better-auth
33
+ * *before* adding Stack: its live tables are still better-auth's own default
34
+ * lowercase names (`user`/`session`/`account`/`verification`), even though the
35
+ * derived list keys need the `Auth` prefix to avoid colliding with the app's
36
+ * own domain `User`. Pass `useBetterAuthTableNames: true` to point every
37
+ * model's physical table at that default while keeping the prefixed list keys
38
+ * (or `tableNames` for an explicit per-model override):
39
+ *
40
+ * ```typescript
41
+ * authPlugin({
42
+ * ...adoptBetterAuthTables({ useBetterAuthTableNames: true }),
43
+ * // AuthUser/AuthSession/AuthAccount/AuthVerification list keys,
44
+ * // @@map("user")/@@map("session")/@@map("account")/@@map("verification")
45
+ * })
46
+ * ```
31
47
  */
32
48
  /** The four better-auth models and their default (unprefixed) model names. */
33
49
  const MODEL_DEFAULT_NAMES = {
@@ -36,6 +52,13 @@ const MODEL_DEFAULT_NAMES = {
36
52
  account: 'Account',
37
53
  verification: 'Verification',
38
54
  };
55
+ /** better-auth's own default lowercase table names, per model. */
56
+ const BETTER_AUTH_DEFAULT_TABLE_NAMES = {
57
+ user: 'user',
58
+ session: 'session',
59
+ account: 'account',
60
+ verification: 'verification',
61
+ };
39
62
  /**
40
63
  * Build the adoption {@link AuthConfig} fragment for a pre-existing better-auth
41
64
  * installation.
@@ -48,11 +71,16 @@ const MODEL_DEFAULT_NAMES = {
48
71
  * (and any field column maps) set to match the live tables
49
72
  */
50
73
  export function adoptBetterAuthTables(options = {}) {
51
- const { schema = 'auth', modelNamePrefix = 'Auth', fields = {} } = options;
74
+ const { schema = 'auth', modelNamePrefix = 'Auth', fields = {}, useBetterAuthTableNames = false, tableNames = {}, } = options;
52
75
  const buildModel = (model) => {
53
76
  const config = {
54
77
  modelName: `${modelNamePrefix}${MODEL_DEFAULT_NAMES[model]}`,
55
78
  };
79
+ const tableName = tableNames[model] ??
80
+ (useBetterAuthTableNames ? BETTER_AUTH_DEFAULT_TABLE_NAMES[model] : undefined);
81
+ if (tableName !== undefined) {
82
+ config.tableName = tableName;
83
+ }
56
84
  const fieldMap = fields[model];
57
85
  if (fieldMap && Object.keys(fieldMap).length > 0) {
58
86
  config.fields = fieldMap;
@@ -1 +1 @@
1
- {"version":3,"file":"adopt-better-auth-tables.js","sourceRoot":"","sources":["../../src/config/adopt-better-auth-tables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AA+DH,8EAA8E;AAC9E,MAAM,mBAAmB,GAAG;IAC1B,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,cAAc;CACpB,CAAA;AAYV;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAAO,GAAiC,EAAE;IAE1C,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,eAAe,GAAG,MAAM,EAAE,MAAM,GAAG,EAAE,EAAE,GAAG,OAAO,CAAA;IAE1E,MAAM,UAAU,GAAG,CAAC,KAAuC,EAAmB,EAAE;QAC9E,MAAM,MAAM,GAAoB;YAC9B,SAAS,EAAE,GAAG,eAAe,GAAG,mBAAmB,CAAC,KAAK,CAAC,EAAE;SAC7D,CAAA;QACD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAC9B,IAAI,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACjD,MAAM,CAAC,MAAM,GAAG,QAAQ,CAAA;QAC1B,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC,CAAA;IAED,OAAO;QACL,MAAM;QACN,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC;QACxB,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC;QAC9B,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC;QAC9B,YAAY,EAAE,UAAU,CAAC,cAAc,CAAC;KACzC,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"adopt-better-auth-tables.js","sourceRoot":"","sources":["../../src/config/adopt-better-auth-tables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAgGH,8EAA8E;AAC9E,MAAM,mBAAmB,GAAG;IAC1B,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,cAAc;CACpB,CAAA;AAEV,kEAAkE;AAClE,MAAM,+BAA+B,GAAG;IACtC,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,cAAc;CACpB,CAAA;AAYV;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAAO,GAAiC,EAAE;IAE1C,MAAM,EACJ,MAAM,GAAG,MAAM,EACf,eAAe,GAAG,MAAM,EACxB,MAAM,GAAG,EAAE,EACX,uBAAuB,GAAG,KAAK,EAC/B,UAAU,GAAG,EAAE,GAChB,GAAG,OAAO,CAAA;IAEX,MAAM,UAAU,GAAG,CAAC,KAAuC,EAAmB,EAAE;QAC9E,MAAM,MAAM,GAAoB;YAC9B,SAAS,EAAE,GAAG,eAAe,GAAG,mBAAmB,CAAC,KAAK,CAAC,EAAE;SAC7D,CAAA;QACD,MAAM,SAAS,GACb,UAAU,CAAC,KAAK,CAAC;YACjB,CAAC,uBAAuB,CAAC,CAAC,CAAC,+BAA+B,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAChF,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,MAAM,CAAC,SAAS,GAAG,SAAS,CAAA;QAC9B,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAC9B,IAAI,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACjD,MAAM,CAAC,MAAM,GAAG,QAAQ,CAAA;QAC1B,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC,CAAA;IAED,OAAO;QACL,MAAM;QACN,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC;QACxB,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC;QAC9B,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC;QAC9B,YAAY,EAAE,UAAU,CAAC,cAAc,CAAC;KACzC,CAAA;AACH,CAAC"}
@@ -2,13 +2,15 @@
2
2
  * Pure `better-auth config → Auth lists` derivation.
3
3
  *
4
4
  * This module is intentionally free of side effects and plugin/runtime
5
- * concerns: given the resolved better-auth model config (per-model `modelName`
6
- * and `fields` column maps) plus any custom User fields, it produces the four
7
- * OpenSaaS Auth lists (user/session/account/verification) with:
5
+ * concerns: given the resolved better-auth model config (per-model `modelName`,
6
+ * `tableName`, and `fields` column maps) plus any custom User fields, it
7
+ * produces the four OpenSaaS Auth lists (user/session/account/verification)
8
+ * with:
8
9
  *
9
10
  * - list keys taken from each model's `modelName`
10
- * - a table `@@map` (list-level `db.map`) when the key differs from the
11
- * default better-auth model name
11
+ * - a table `@@map` (list-level `db.map`) taken from each model's resolved
12
+ * `tableName` independent of `modelName`, so a renamed list key can still
13
+ * adopt a differently-named live table
12
14
  * - field-level `@map` (`db.map`) for any better-auth field → column override
13
15
  * - relationship refs between the auth lists wired to the *derived* keys
14
16
  * (e.g. `Session.user → AuthUser.sessions`)
@@ -1 +1 @@
1
- {"version":3,"file":"derive-auth-lists.d.ts","sourceRoot":"","sources":["../../src/config/derive-auth-lists.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAA6B,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAanG;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,oDAAoD;IACpD,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;QACf,YAAY,EAAE,MAAM,CAAA;KACrB,CAAA;IACD,kEAAkE;IAElE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;CACvC,CAAA;AAyMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,oBAAoB,EAC5B,UAAU,GAAE,oBAAyB,EACrC,YAAY,GAAE,gBAAqB,GAClC,gBAAgB,CAiBlB"}
1
+ {"version":3,"file":"derive-auth-lists.d.ts","sourceRoot":"","sources":["../../src/config/derive-auth-lists.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAA6B,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEnG;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,oDAAoD;IACpD,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;QACf,YAAY,EAAE,MAAM,CAAA;KACrB,CAAA;IACD,kEAAkE;IAElE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;CACvC,CAAA;AAiND;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,oBAAoB,EAC5B,UAAU,GAAE,oBAAyB,EACrC,YAAY,GAAE,gBAAqB,GAClC,gBAAgB,CAiBlB"}