@opensaas/stack-auth 0.39.2 → 0.41.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +153 -0
- package/CLAUDE.md +140 -22
- package/dist/client/index.d.ts +0 -16
- package/dist/client/index.d.ts.map +1 -1
- package/dist/config/adopt-better-auth-tables.d.ts +4 -49
- package/dist/config/adopt-better-auth-tables.d.ts.map +1 -1
- package/dist/config/adopt-better-auth-tables.js +4 -51
- package/dist/config/adopt-better-auth-tables.js.map +1 -1
- package/dist/config/derive-auth-lists.d.ts +42 -37
- package/dist/config/derive-auth-lists.d.ts.map +1 -1
- package/dist/config/derive-auth-lists.js +579 -223
- package/dist/config/derive-auth-lists.js.map +1 -1
- package/dist/config/index.d.ts +0 -3
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +4 -34
- package/dist/config/index.js.map +1 -1
- package/dist/config/plugin.d.ts.map +1 -1
- package/dist/config/plugin.js +20 -90
- package/dist/config/plugin.js.map +1 -1
- package/dist/config/types.d.ts +89 -67
- package/dist/config/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -13
- package/dist/index.js.map +1 -1
- package/dist/lists/index.d.ts +9 -5
- package/dist/lists/index.d.ts.map +1 -1
- package/dist/lists/index.js +9 -6
- package/dist/lists/index.js.map +1 -1
- package/dist/mcp/better-auth.d.ts +6 -19
- package/dist/mcp/better-auth.d.ts.map +1 -1
- package/dist/mcp/better-auth.js +6 -20
- package/dist/mcp/better-auth.js.map +1 -1
- package/dist/plugins/index.d.ts +4 -4
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +4 -4
- package/dist/plugins/index.js.map +1 -1
- package/dist/runtime/types.d.ts +2 -9
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js +0 -4
- package/dist/runtime/types.js.map +1 -1
- package/dist/server/index.d.ts +5 -12
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +14 -62
- package/dist/server/index.js.map +1 -1
- package/dist/ui/components/ForgotPasswordForm.d.ts +0 -3
- package/dist/ui/components/ForgotPasswordForm.d.ts.map +1 -1
- package/dist/ui/components/ForgotPasswordForm.js +0 -3
- package/dist/ui/components/ForgotPasswordForm.js.map +1 -1
- package/dist/ui/components/ResetPasswordForm.d.ts +0 -3
- package/dist/ui/components/ResetPasswordForm.d.ts.map +1 -1
- package/dist/ui/components/ResetPasswordForm.js +0 -5
- package/dist/ui/components/ResetPasswordForm.js.map +1 -1
- package/dist/ui/components/SignInForm.d.ts +0 -3
- package/dist/ui/components/SignInForm.d.ts.map +1 -1
- package/dist/ui/components/SignInForm.js +0 -4
- package/dist/ui/components/SignInForm.js.map +1 -1
- package/dist/ui/components/SignUpForm.d.ts +0 -3
- package/dist/ui/components/SignUpForm.d.ts.map +1 -1
- package/dist/ui/components/SignUpForm.js +0 -5
- package/dist/ui/components/SignUpForm.js.map +1 -1
- package/dist/ui/lib/clean-error-message.d.ts +3 -6
- package/dist/ui/lib/clean-error-message.d.ts.map +1 -1
- package/dist/ui/lib/clean-error-message.js +3 -6
- package/dist/ui/lib/clean-error-message.js.map +1 -1
- package/dist/ui/types.d.ts +2 -15
- package/dist/ui/types.d.ts.map +1 -1
- package/dist/ui/types.js +2 -7
- package/dist/ui/types.js.map +1 -1
- package/package.json +12 -5
- package/src/client/index.ts +0 -17
- package/src/config/adopt-better-auth-tables.ts +4 -51
- package/src/config/derive-auth-lists.ts +685 -252
- package/src/config/index.ts +4 -34
- package/src/config/plugin.ts +21 -91
- package/src/config/types.ts +90 -67
- package/src/index.ts +3 -14
- package/src/lists/index.ts +17 -5
- package/src/mcp/better-auth.ts +6 -25
- package/src/plugins/index.ts +4 -5
- package/src/runtime/types.ts +2 -10
- package/src/server/index.ts +17 -70
- package/src/ui/components/ForgotPasswordForm.tsx +0 -3
- package/src/ui/components/ResetPasswordForm.tsx +0 -5
- package/src/ui/components/SignInForm.tsx +0 -4
- package/src/ui/components/SignUpForm.tsx +0 -5
- package/src/ui/lib/clean-error-message.ts +3 -6
- package/src/ui/types.ts +2 -15
- package/tests/auth-lists-drift.test.ts +480 -0
- package/tests/config.test.ts +3 -0
- package/tests/credential-field-read-deny-e2e.test.ts +295 -0
- package/tests/derive-auth-lists.test.ts +263 -6
- package/tests/generated-fk-shape.test.ts +432 -9
- package/tests/mcp-oauth-cascade-e2e.test.ts +267 -0
- package/tests/plugin-derived-keys.test.ts +39 -0
- package/tests/plugin-table-derivation.test.ts +270 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/server/schema-converter.d.ts +0 -81
- package/dist/server/schema-converter.d.ts.map +0 -1
- package/dist/server/schema-converter.js +0 -157
- package/dist/server/schema-converter.js.map +0 -1
- package/src/server/schema-converter.ts +0 -243
- package/tests/schema-converter.test.ts +0 -399
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,158 @@
|
|
|
1
1
|
# @opensaas/stack-auth
|
|
2
2
|
|
|
3
|
+
## 0.41.0
|
|
4
|
+
|
|
5
|
+
## 0.40.0
|
|
6
|
+
|
|
7
|
+
### Minor Changes
|
|
8
|
+
|
|
9
|
+
- [#1015](https://github.com/OpenSaasAU/stack/pull/1015) [`72c4ba3`](https://github.com/OpenSaasAU/stack/commit/72c4ba30e9f53762988a822bb7ead7eae0db270c) Thanks [@borisno2](https://github.com/borisno2)! - Bump the `better-auth` dev dependency to `1.7.1` and move the peer range off the stale `^1.3.29` floor to `^1.4.0` (the release line where better-auth's `index: true` flags — the basis for [#937](https://github.com/OpenSaasAU/stack/issues/937)'s index emission — first shipped; below it the generated schema silently omitted indexes better-auth itself declares).
|
|
10
|
+
|
|
11
|
+
**New required `account.issuer` column.** better-auth 1.7 adds a required `issuer` column to its `account` model. Because `deriveAuthLists` derives the Auth lists from better-auth's own `getAuthTables()` ([#987](https://github.com/OpenSaasAU/stack/issues/987)/[#997](https://github.com/OpenSaasAU/stack/issues/997)), this column now appears in the generated schema automatically — no code change was needed, only verification against real generated output. Existing projects upgrading to `better-auth@^1.7` will see a **new NOT NULL column** on their `account` table and need a backfill for existing rows. Following better-auth's own `createLocalAccountIssuer`/`createOAuthAccountIssuer` helpers (`@better-auth/core/db`):
|
|
12
|
+
|
|
13
|
+
```sql
|
|
14
|
+
-- PostgreSQL / SQLite (|| is string concatenation on both)
|
|
15
|
+
UPDATE "Account" SET issuer = 'local:' || "providerId" WHERE issuer IS NULL AND "providerId" = 'credential';
|
|
16
|
+
UPDATE "Account" SET issuer = 'local:oauth:' || "providerId" WHERE issuer IS NULL AND "providerId" != 'credential';
|
|
17
|
+
|
|
18
|
+
-- MySQL (|| is logical OR by default, NOT concatenation — use CONCAT instead)
|
|
19
|
+
UPDATE `Account` SET issuer = CONCAT('local:', providerId) WHERE issuer IS NULL AND providerId = 'credential';
|
|
20
|
+
UPDATE `Account` SET issuer = CONCAT('local:oauth:', providerId) WHERE issuer IS NULL AND providerId != 'credential';
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`"Account"`/`` `Account` `` above is the stack's own greenfield default table name — substitute your project's actual (and, on Postgres, schema-qualified) table name if you renamed it via `authPlugin({ account: { tableName } })` or adopted an existing install with `adoptBetterAuthTables({ useBetterAuthTableNames: true })` (physical table `account`, commonly under a non-`public` schema).
|
|
24
|
+
|
|
25
|
+
URL-encode `providerId` if it can contain characters outside `[A-Za-z0-9_-]`. If you configured a custom OIDC provider with its own issuer URL, use that provider's real issuer instead of the synthetic `local:oauth:` value.
|
|
26
|
+
|
|
27
|
+
**The new `@@unique([issuer, accountId])` constraint is not yet emitted.** better-auth 1.7 also declares this composite unique index at the table level, but the stack only derives _field_-level `unique`/`index` flags today — table-level index derivation is [#985](https://github.com/OpenSaasAU/stack/issues/985), which hasn't landed. This is deliberately out of scope here (per [#986](https://github.com/OpenSaasAU/stack/issues/986)'s own triage note: build on [#985](https://github.com/OpenSaasAU/stack/issues/985) once it lands, don't duplicate it). When it does land, make sure your backfilled `issuer` values don't collide on `(issuer, accountId)` for any account, or the constraint will fail to apply.
|
|
28
|
+
|
|
29
|
+
**Breaking (MCP plugin users only): `@better-auth/mcp` is now a separate package.** better-auth 1.7 split the `mcp` plugin out of `better-auth/plugins` into its own package, rebuilt on the OAuth Provider RFC 8707/9728 resource model. `@opensaas/stack-auth/plugins` now re-exports `mcp` from `@better-auth/mcp` (added as an optional peer — install it if you use MCP). The plugin also now **requires** a `resource` option:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { mcp } from '@opensaas/stack-auth/plugins'
|
|
33
|
+
import { jwt } from 'better-auth/plugins'
|
|
34
|
+
|
|
35
|
+
authPlugin({
|
|
36
|
+
betterAuthPlugins: [
|
|
37
|
+
// The OAuth Provider mcp() is built on issues JWT-based access tokens
|
|
38
|
+
// and requires better-auth's own jwt() plugin registered alongside it —
|
|
39
|
+
// omitting it throws `BetterAuthError: jwt_config` at init.
|
|
40
|
+
jwt(),
|
|
41
|
+
mcp({
|
|
42
|
+
loginPage: '/sign-in',
|
|
43
|
+
// The page where a user approves/denies an MCP client's requested
|
|
44
|
+
// scopes — also required as of better-auth 1.7.
|
|
45
|
+
consentPage: '/consent',
|
|
46
|
+
// RFC 8707/9728 canonical resource identifier — required as of
|
|
47
|
+
// better-auth 1.7. Must match your `mcp.basePath`. HTTP is only
|
|
48
|
+
// accepted on loopback hosts.
|
|
49
|
+
resource: `${process.env.BETTER_AUTH_URL}/api/mcp`,
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
better-auth 1.7's MCP plugin also declares a substantially different OAuth table set — the old `oauthApplication`/`oauthAccessToken`/`oauthConsent` three became seven tables (`oauthClient`/`oauthAccessToken`/`oauthConsent`/`oauthRefreshToken`/`oauthResource`/`oauthClientResource`/`oauthClientAssertion`). Since the derivation is schema-driven this needed no code changes, but if you have the MCP plugin enabled, running `pnpm generate` will produce a significantly different Prisma schema for these tables (new/renamed models, and `Session` gains reverse relations to the two token tables that now reference it). Review the diff and migrate your database accordingly.
|
|
56
|
+
|
|
57
|
+
**Workspace-wide: pins `@better-auth/utils` to `0.5.0` via a root `pnpm.overrides`.** better-auth 1.7.1's own published packages disagree on this transitive dependency — `better-auth` pins it at exactly `0.4.2` while `better-call` (used by `@better-auth/core`, `@better-auth/oauth-provider`, and `@better-auth/mcp`) requires `^0.5.0` — so pnpm resolves two separate physical instances of `@better-auth/core` depending on which peer chain a given package sits in. That split is invisible at runtime but breaks TypeScript: `jwt()` (from `better-auth/plugins`) and `mcp()` (from `@better-auth/mcp`) end up typed against different `@better-auth/core` instances, so `betterAuthPlugins: [jwt(), mcp(...)]` fails to type-check with a `BetterAuthPlugin` structural-mismatch error even though both plugins are otherwise correctly configured. The override forces one instance workspace-wide. If you hit the same error in your own app, add the equivalent override to your own `package.json`.
|
|
58
|
+
|
|
59
|
+
Also fixes two `deriveAuthLists` gaps surfaced by the MCP plugin's expanded schema:
|
|
60
|
+
|
|
61
|
+
- the scalar-field builder now threads a static `defaultValue` through for `number`-typed fields (`integer()`/`bigInt()`), matching the existing `string`/`boolean` behavior (e.g. `oauthResource.policyVersion`, which defaults to `1`)
|
|
62
|
+
- a plugin table that declares only one of `createdAt`/`updatedAt` upstream (several of the new OAuth tables declare `createdAt` alone) is no longer silently dropped — it derives as an ordinary required column instead. Previously any model with an asymmetric timestamp pair had the field skipped entirely with no replacement, which crashed the first real write that supplied it (better-auth's own OAuth Provider does this at `betterAuth()` init time, seeding an `oauthResource` row)
|
|
63
|
+
|
|
64
|
+
- [#1019](https://github.com/OpenSaasAU/stack/pull/1019) [`77b7314`](https://github.com/OpenSaasAU/stack/commit/77b731452f24045995a1d3a2ffede0246d5743d3) Thanks [@borisno2](https://github.com/borisno2)! - Extend the ADR-0036 credential field read-deny from the four base Auth models to better-auth plugin tables, and add a `credentialFields` config option for plugins the stack doesn't seed a set for.
|
|
65
|
+
|
|
66
|
+
The following fields now ship field-level `read`-denied, on top of the existing `Session.token`/`Verification.value`/`Account.password`/`accessToken`/`refreshToken`/`idToken`:
|
|
67
|
+
|
|
68
|
+
- `oauthClient.clientSecret`, `oauthAccessToken.token`, `oauthRefreshToken.token` (the `mcp`/oauth-provider plugin)
|
|
69
|
+
- `twoFactor.secret`, `twoFactor.backupCodes` (`twoFactor()`)
|
|
70
|
+
|
|
71
|
+
An application opening one of these lists (e.g. declaring `OauthClient` under its own `lists` to grant access) no longer also exposes the credential column — same behavior as the existing base-model deny, `sudo()` still reads it.
|
|
72
|
+
|
|
73
|
+
For a plugin the stack has no seeded credential set for, mark a field yourself:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
authPlugin({
|
|
77
|
+
betterAuthPlugins: [passkey()],
|
|
78
|
+
credentialFields: { passkey: ['publicKey'] },
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`credentialFields` is additive only — it can add fields to any model (including a seeded one) but can never unmark a seeded field. An entry naming a field missing from a model your app actually derives throws at config time; an entry for a model your app doesn't derive is a no-op.
|
|
83
|
+
|
|
84
|
+
- [#1017](https://github.com/OpenSaasAU/stack/pull/1017) [`b30fa61`](https://github.com/OpenSaasAU/stack/commit/b30fa6135a6acca8c9be99fbdf5ffa7faab1959f) Thanks [@{](https://github.com/{)! - Let an application declare model-level indexes (`db.indexes`) on the derived auth lists (`User`/`Session`/`Account`/`Verification`/`RateLimit`).
|
|
85
|
+
|
|
86
|
+
Each per-model block in `authPlugin()` now accepts `indexes`, in the same shape as a list's own `db.indexes`:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
authPlugin({
|
|
90
|
+
// Adopt a live constraint's real name instead of Prisma's derived one.
|
|
91
|
+
indexes: [{ fields: ['email'], unique: true, name: 'user_email_key' }] },
|
|
92
|
+
session: { indexes: [{ fields: ['token'], unique: true, name: 'session_token_key' }] },
|
|
93
|
+
// Extend a derived column into a composite index.
|
|
94
|
+
verification: {
|
|
95
|
+
indexes: [{ fields: ['identifier', { field: 'createdAt', sort: 'desc' }] }],
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
An entry covering a column the stack already derives an index for (e.g. `User.email`) suppresses that derived index for that column and emits only the app's entry, rather than erroring — the application's declaration wins (ADR-0035). Suppression is per-column: every other derived index on the model is unaffected.
|
|
101
|
+
|
|
102
|
+
This also fixes a related generator gap: a list's `db.indexes` can now reference `createdAt`/`updatedAt` even when the list has no explicit field for them and relies on `db.timestamps` for the auto-injected columns (previously only a list with an explicitly declared `createdAt`/`updatedAt` field could be indexed on it).
|
|
103
|
+
|
|
104
|
+
- [#1005](https://github.com/OpenSaasAU/stack/pull/1005) [`b67fdd1`](https://github.com/OpenSaasAU/stack/commit/b67fdd10d678f9fd209259b063186db9f9aaf20a) Thanks [@borisno2](https://github.com/borisno2)! - Better-auth plugin tables (e.g. the MCP plugin's `oauthApplication`/`oauthAccessToken`/`oauthConsent`) are now derived through the same registry as the four base Auth models, instead of a separate converter that dropped every reference to a bare column. Reference fields now become real `relationship()` foreign keys with the correct `onDelete` cascade, index, uniqueness and nullability, closing a data-integrity defect where deleting a user left their OAuth rows orphaned (neither the database nor better-auth's own `deleteUser` cleaned them up). Plugin-table scalar fields now also honour `fieldName` column maps and `index: true`, and list keys are PascalCased with `db.map` restoring the original physical table name. A reference whose target field isn't the target's `id` (e.g. `oauthAccessToken.clientId` → `oauthApplication.clientId`) is left as a plain scalar column, since `relationship()` only supports `id`-based foreign keys.
|
|
105
|
+
|
|
106
|
+
No config changes are required — `authPlugin()`/`getAuthLists()` are unchanged. If your app already had an MCP-enabled config generated with an older version, regenerate and diff your schema: the OAuth tables' `userId` columns gain a foreign key, cascade and index they didn't have before.
|
|
107
|
+
|
|
108
|
+
- [#1013](https://github.com/OpenSaasAU/stack/pull/1013) [`49687ea`](https://github.com/OpenSaasAU/stack/commit/49687eaf8ad80696d62e2616ba3dfef992985282) Thanks [@borisno2](https://github.com/borisno2)! - BREAKING (pre-1.0): The derived auth lists' credential-bearing fields now ship with a field-level `read` deny, so opening operation-level access to a list no longer exposes them:
|
|
109
|
+
|
|
110
|
+
- `Session.token`
|
|
111
|
+
- `Verification.value`
|
|
112
|
+
- `Account.password`
|
|
113
|
+
- `Account.accessToken`
|
|
114
|
+
- `Account.refreshToken`
|
|
115
|
+
- `Account.idToken`
|
|
116
|
+
|
|
117
|
+
A denied field is stripped from a returned row, not an error — a `context.db` read on an opened list still succeeds and returns every other field, including a `findUnique` lookup that selects the row **by** the denied field itself (e.g. `context.db.session.findUnique({ where: { token } })` still finds the session; the returned `token` comes back stripped). Naming a denied field in `findMany`'s (or `count`'s) `where`/`orderBy` is different: the existing predicate-time read-access check (`validateQueryFieldReadAccess`) throws a `ValidationError` there instead, the same as it already does for any other field-level `read` deny. `sudo()` bypasses both — it's the supported path for an application with a genuine need. Sign-in, session refresh, email verification, and password reset are unaffected — better-auth's own flows write through the raw Prisma adapter, bypassing access control entirely.
|
|
118
|
+
|
|
119
|
+
If your application opens one of these lists today and deliberately reads one of these fields through `context.db` — a returned row, a `findMany`/`count` predicate, or a `findUnique` selector — switch that access to `context.sudo().db...`. See ADR-0036.
|
|
120
|
+
|
|
121
|
+
### Patch Changes
|
|
122
|
+
|
|
123
|
+
- [#1020](https://github.com/OpenSaasAU/stack/pull/1020) [`8e6707a`](https://github.com/OpenSaasAU/stack/commit/8e6707adcca9d7e062bc1747ec79a29082c09ef9) Thanks [@borisno2](https://github.com/borisno2)! - Read-denied credential fields (ADR-0036) now also declare `ui.listView.defaultColumn: false`, so they're curated out of the admin's default table columns instead of rendering as permanently empty columns.
|
|
124
|
+
|
|
125
|
+
- [#997](https://github.com/OpenSaasAU/stack/pull/997) [`ed6ffcd`](https://github.com/OpenSaasAU/stack/commit/ed6ffcd5cc2b471ea680f75f108596ee6b87d083) Thanks [@borisno2](https://github.com/borisno2)! - `deriveAuthLists` now derives the Auth lists (User/Session/Account/Verification/RateLimit) from better-auth's own `getAuthTables()` output instead of a hand-written transcription, closing the drift class behind [#935](https://github.com/OpenSaasAU/stack/issues/935)/[#937](https://github.com/OpenSaasAU/stack/issues/937)/[#921](https://github.com/OpenSaasAU/stack/issues/921)/[#986](https://github.com/OpenSaasAU/stack/issues/986). Generated schema output is unchanged for existing projects — no migration needed.
|
|
126
|
+
|
|
127
|
+
- [#972](https://github.com/OpenSaasAU/stack/pull/972) [`08c3787`](https://github.com/OpenSaasAU/stack/commit/08c3787a46ead83bbc6a3730dae4d89598fba1b2) Thanks [@borisno2](https://github.com/borisno2)! - Clean up comments in `packages/auth/src` per the CLAUDE.md Comments rule — removed restating/duplicated comments, kept public-API TSDoc and footgun/external-constraint warnings. No behavior changes.
|
|
128
|
+
|
|
129
|
+
- [#996](https://github.com/OpenSaasAU/stack/pull/996) [`cfd366c`](https://github.com/OpenSaasAU/stack/commit/cfd366ccb62c3a858a95d0df859984c08e3b3a5f) Thanks [@borisno2](https://github.com/borisno2)! - Add a test that compares the derived Auth lists against better-auth's own `getAuthTables()` definitions, failing the build on future upstream schema drift instead of relying on a human to notice.
|
|
130
|
+
|
|
131
|
+
- [#990](https://github.com/OpenSaasAU/stack/pull/990) [`37d7905`](https://github.com/OpenSaasAU/stack/commit/37d7905b9b5126e7d7826469af467775f4daab34) Thanks [@borisno2](https://github.com/borisno2)! - The derived `Session.user` / `Account.user` foreign keys are now indexed (`isIndexed: true`, was `false`), and `Verification.identifier` is now indexed too — matching the three indexes better-auth itself declares (`session_userId_idx`, `account_userId_idx`, `verification_identifier_idx`). `prisma migrate diff` against a real better-auth install no longer reads these as three dropped indexes, and `Session`/`Account` lookups by `userId` are no longer unindexed.
|
|
132
|
+
|
|
133
|
+
**Migration note:** existing projects will see a migration on their next `prisma migrate dev`/`db push` adding the three indexes.
|
|
134
|
+
|
|
135
|
+
- [#989](https://github.com/OpenSaasAU/stack/pull/989) [`9cc6f8d`](https://github.com/OpenSaasAU/stack/commit/9cc6f8dcb0ed2958c39da9e7648a7a462c10264a) Thanks [@borisno2](https://github.com/borisno2)! - Fix `Session.user`/`Account.user` foreign key generating a `user` physical column instead of `userId`, mismatching better-auth's own schema and breaking clean-diff adoption (ADR-0007). An explicit `fields: { userId: ... }` override is unaffected.
|
|
136
|
+
|
|
137
|
+
**Migration note:** existing greenfield projects need to rename the column on `Session` and `Account` (e.g. `ALTER TABLE "Session" RENAME COLUMN "user" TO "userId";` and the same for `Account`) to match the new generated schema.
|
|
138
|
+
|
|
139
|
+
- [#1002](https://github.com/OpenSaasAU/stack/pull/1002) [`48d2762`](https://github.com/OpenSaasAU/stack/commit/48d27626dfb636c481301116e46c826ef3156124) Thanks [@borisno2](https://github.com/borisno2)! - Fix admin UI URL round-trip for a list keyed with anything other than strict PascalCase (issue [#991](https://github.com/OpenSaasAU/stack/issues/991)). `getListKeyFromUrl` reconstructs a list key by string transformation, which is lossy for a non-PascalCase key — a real example is a better-auth plugin's derived list (e.g. `oauthApplication`, from the `mcp` plugin's OAuth tables). Such a list appeared in navigation but its own link resolved to a key that did not exist in `config.lists`, rendering "List not found".
|
|
140
|
+
|
|
141
|
+
`@opensaas/stack-core` adds `resolveListKeyFromUrl(urlSegment, listKeys)` alongside the existing `getListKeyFromUrl`, which is unchanged and still exported. The new resolver matches a URL segment against the config's actual list keys via `getUrlKey` — the same helper that builds the URL — instead of reconstructing one, so route lookup and URL generation cannot drift apart. It returns `undefined` for a segment matching no list (so callers keep rendering their existing "not found" state), and throws if two distinct list keys would produce the same URL segment.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { resolveListKeyFromUrl } from '@opensaas/stack-core'
|
|
145
|
+
|
|
146
|
+
resolveListKeyFromUrl('oauth-application', Object.keys(config.lists)) // 'oauthApplication'
|
|
147
|
+
resolveListKeyFromUrl('does-not-exist', Object.keys(config.lists)) // undefined
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`@opensaas/stack-ui`'s `AdminUI` now uses `resolveListKeyFromUrl` for its route resolution, fixing the broken link for any such list.
|
|
151
|
+
|
|
152
|
+
`@opensaas/stack-auth`'s `convertBetterAuthSchema` now PascalCases a better-auth plugin's camelCase `modelName` when deriving a list key (`oauthApplication` → `OauthApplication`, `rateLimit` → `RateLimit`), matching the repo's PascalCase list-key convention and fixing the same round-trip bug at the source for these lists.
|
|
153
|
+
|
|
154
|
+
**Schema-affecting for `@opensaas/stack-auth` users with a better-auth plugin that declares extra tables** (e.g. `mcp`'s OAuth tables, or `rateLimit.storage: 'database'` with no `modelName` remap configured): the generated Prisma **model name** changes to match the new PascalCase list key. The physical **table name** does not change — the previous camelCase name is preserved via `db.map` (`@@map`) — so `prisma db push` / `prisma migrate dev` sees a model rename, not a table rename, and `context.db.oauthApplication` (the camelCase db accessor) keeps working unchanged. Regenerate (`pnpm generate`) and re-run your migration/push step after upgrading.
|
|
155
|
+
|
|
3
156
|
## 0.39.2
|
|
4
157
|
|
|
5
158
|
## 0.39.1
|
package/CLAUDE.md
CHANGED
|
@@ -49,7 +49,16 @@ Pre-built forms (client components). Each takes **server action** props (not an
|
|
|
49
49
|
|
|
50
50
|
### Plugins (`src/plugins/index.ts`)
|
|
51
51
|
|
|
52
|
-
-
|
|
52
|
+
- `mcp` - re-exported from the optional `@better-auth/mcp` peer (better-auth
|
|
53
|
+
1.7 split it out of `better-auth/plugins`) for OAuth authentication with AI
|
|
54
|
+
assistants. Its `resource` option (the RFC 8707/9728 canonical
|
|
55
|
+
protected-resource URL) and `consentPage` (where a user approves/denies an
|
|
56
|
+
MCP client's requested scopes) are both required as of that split, and it
|
|
57
|
+
now needs better-auth's own `jwt()` plugin (from `better-auth/plugins`,
|
|
58
|
+
unaffected by the `@better-auth/mcp` split) registered alongside it — the
|
|
59
|
+
OAuth Provider `mcp()` is built on issues JWT-based access tokens and
|
|
60
|
+
throws `BetterAuthError: jwt_config` at init without it. See the
|
|
61
|
+
`betterAuthPlugins` example in the root `CLAUDE.md`'s MCP section.
|
|
53
62
|
|
|
54
63
|
## Architecture Patterns
|
|
55
64
|
|
|
@@ -67,22 +76,65 @@ config({
|
|
|
67
76
|
|
|
68
77
|
### Deriving Auth lists from better-auth config
|
|
69
78
|
|
|
70
|
-
The four Auth lists
|
|
71
|
-
|
|
72
|
-
`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
-
|
|
79
|
+
The four Auth lists, the conditional `RateLimit` fifth, and every table a
|
|
80
|
+
better-auth plugin declares in its own `schema` (e.g. the `mcp` plugin's
|
|
81
|
+
`oauthClient`/`oauthAccessToken`/`oauthConsent`/`oauthRefreshToken`/
|
|
82
|
+
`oauthResource`/`oauthClientResource`/`oauthClientAssertion` — better-auth
|
|
83
|
+
1.7 split the plugin into `@better-auth/mcp` and rebuilt it on the OAuth
|
|
84
|
+
Provider RFC 8707/9728 resource model, a wider table set than the pre-1.7
|
|
85
|
+
`oauthApplication`/`oauthAccessToken`/`oauthConsent` three) are all **derived**
|
|
86
|
+
from better-auth's own resolved table definitions, not hand-transcribed. The
|
|
87
|
+
pure derivation lives in `src/config/derive-auth-lists.ts` (`deriveAuthLists`),
|
|
88
|
+
which `getAuthLists` and the plugin's `init` consume. It calls `getAuthTables`
|
|
89
|
+
(re-exported from `better-auth/db` — the same function better-auth's own
|
|
90
|
+
Kysely migrator, schema generator, and adapter base use to build their
|
|
91
|
+
schemas), passing the app's `betterAuthPlugins` through as `options.plugins`
|
|
92
|
+
so a plugin's own schema (a standalone table, or a schema extension of a base
|
|
93
|
+
model like the `anonymous` plugin's `user.isAnonymous`) is already merged
|
|
94
|
+
into the result — and translates it into stack list configs, so the Auth
|
|
95
|
+
lists cannot silently drift from what better-auth itself declares (issue
|
|
96
|
+
#987, ADR-0033, ADR-0034):
|
|
97
|
+
|
|
98
|
+
- per-model `modelName`/`fields` (the developer's own `authPlugin({ user:
|
|
99
|
+
{...}, session: {...} })` config, already normalized) pass straight
|
|
100
|
+
through to `getAuthTables` as its own options object, so an override path
|
|
101
|
+
(`adoptBetterAuthTables`, a renamed model, a remapped column) is inherited
|
|
102
|
+
for free rather than re-implemented as a parallel normalization
|
|
103
|
+
- `modelName` → list key (and Prisma model name); `tableName` → table
|
|
104
|
+
`@@map`, **independent of `modelName`** (defaults to `modelName` when it
|
|
105
|
+
differs from the better-auth default, otherwise unset — i.e. unchanged
|
|
106
|
+
output when `tableName` isn't set)
|
|
107
|
+
- every scalar field's type, nullability, uniqueness, index, column map, and
|
|
108
|
+
static default value are read from better-auth's own field metadata (a
|
|
109
|
+
fixed type-mapping table: `string`→`text()`, `boolean`→`checkbox()`,
|
|
110
|
+
`date`→`timestamp()`, `number`→`integer()`/`bigInt()` depending on
|
|
111
|
+
better-auth's own `bigint` flag) — not hand-authored per field
|
|
112
|
+
- the `user` relationship foreign key maps to better-auth's own resolved
|
|
113
|
+
column name (`userId` by default; an explicit `fields.userId` override
|
|
114
|
+
takes precedence), and its index/uniqueness and `onDelete` action are read
|
|
115
|
+
from better-auth's own `index`/`unique`/`references.onDelete`, matching a
|
|
116
|
+
live better-auth database on all three dimensions (ADR-0007, ADR-0033)
|
|
81
117
|
- relationship refs between the Auth lists follow the derived keys
|
|
82
|
-
(e.g. `Session.user → AuthUser.sessions`)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
118
|
+
(e.g. `Session.user → AuthUser.sessions`) — the reverse relation name
|
|
119
|
+
itself (`sessions`/`accounts`) has no source in better-auth's metadata
|
|
120
|
+
(its FK declaration is one-directional) and is derived by pluralizing the
|
|
121
|
+
child model's own key, with a documented override map in
|
|
122
|
+
`derive-auth-lists.ts` for a collision or bad pluralization
|
|
123
|
+
- a **plugin table**'s list key is PascalCased from better-auth's resolved
|
|
124
|
+
`modelName` (`oauthClient` → `OauthClient`), with `db.map` set
|
|
125
|
+
back to the original whenever the case changed; its scalar/FK fields go
|
|
126
|
+
through the exact same derivation as a base model's, including the reverse
|
|
127
|
+
relation onto whichever list it references (base or another plugin table).
|
|
128
|
+
A reference whose target field isn't the target's `id` (better-auth's own
|
|
129
|
+
oidc-provider schema does this — `oauthAccessToken.clientId` references
|
|
130
|
+
`oauthClient.clientId`, not its `id`) stays a plain scalar column,
|
|
131
|
+
since `relationship()` can only express an `id`-based FK. Plugin tables
|
|
132
|
+
ship closed like the base models, with no `access` passthrough at all —
|
|
133
|
+
see ADR-0034 and "Access control on Auth lists" below
|
|
134
|
+
|
|
135
|
+
With no `modelName`/`tableName`/`fields` overrides the base list/table shape
|
|
136
|
+
is otherwise unchanged (`User`/`Session`/`Account`/`Verification`, original
|
|
137
|
+
field shapes, no table `@@map`).
|
|
86
138
|
|
|
87
139
|
```typescript
|
|
88
140
|
// Adopt an existing better-auth installation (Auth lists ≠ app User)
|
|
@@ -133,12 +185,78 @@ reason — "who is this session" must not depend on the application's User
|
|
|
133
185
|
access policy. `sudo` is a plain second argument, not a method on `context`
|
|
134
186
|
(`AccessContext`) itself — see `packages/core/CLAUDE.md`.
|
|
135
187
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
188
|
+
Better-auth plugin tables (e.g. OAuth client tables from the `mcp` plugin)
|
|
189
|
+
are derived by the same `deriveAuthLists` — not a separate converter, since
|
|
190
|
+
ADR-0034 — and also ship closed: there is no `access` passthrough for them;
|
|
191
|
+
an app that needs to grant access declares the list itself under the same
|
|
192
|
+
derived key so the plugin's field-only extend path merges in (its own access
|
|
193
|
+
then stands, same as any other `extendList`).
|
|
194
|
+
|
|
195
|
+
### Credential fields are read-denied independent of operation access (ADR-0036)
|
|
196
|
+
|
|
197
|
+
Operation-level access is all-or-nothing at the list, not the column — so
|
|
198
|
+
the deny above (list-level) isn't the whole story for a credential-bearing
|
|
199
|
+
field: `deriveAuthLists` sets a field-level `access: { read: () => false }`
|
|
200
|
+
on each one unconditionally, independent of whatever `accessConfig` an app
|
|
201
|
+
supplies. Opening `query` on `Session` for a "your active sessions" screen no
|
|
202
|
+
longer also exposes `token` — the field is stripped from a returned row (the
|
|
203
|
+
ordinary field-access-denial behavior), the row itself still returns. This
|
|
204
|
+
holds even for a `findUnique` lookup that selects the row BY the denied
|
|
205
|
+
field (`context.db.session.findUnique({ where: { token } })` still finds
|
|
206
|
+
the session; `token` just comes back stripped) — `findUnique`'s `where` is
|
|
207
|
+
a unique selector, not a predicate the read-access check walks. Naming the
|
|
208
|
+
field in `findMany`'s (or `count`'s) `where`/`orderBy` instead takes the
|
|
209
|
+
predicate-time path (`validateQueryFieldReadAccess` in
|
|
210
|
+
`packages/core/src/access/query-validation.ts`) and throws a
|
|
211
|
+
`ValidationError` up front rather than stripping anything; `sudo()` is
|
|
212
|
+
required for that shape too, not only for reading the column back off a
|
|
213
|
+
row fetched another way.
|
|
214
|
+
|
|
215
|
+
This is not a closed list of six base-model fields — it also covers **plugin
|
|
216
|
+
table** credential fields the stack has first-class support for (ADR-0034),
|
|
217
|
+
since a plugin table derives through the same scalar-field derivation pass:
|
|
218
|
+
|
|
219
|
+
| Model (better-auth key) | Field(s) | Source |
|
|
220
|
+
| ----------------------- | ---------------------------------------------------- | ---------------------- |
|
|
221
|
+
| `session` | `token` | base |
|
|
222
|
+
| `verification` | `value` | base |
|
|
223
|
+
| `account` | `password`, `accessToken`, `refreshToken`, `idToken` | base |
|
|
224
|
+
| `oauthClient` | `clientSecret` | `mcp` / oauth-provider |
|
|
225
|
+
| `oauthAccessToken` | `token` | `mcp` / oauth-provider |
|
|
226
|
+
| `oauthRefreshToken` | `token` | `mcp` / oauth-provider |
|
|
227
|
+
| `twoFactor` | `secret`, `backupCodes` | `twoFactor()` |
|
|
228
|
+
|
|
229
|
+
The deny is applied in the scalar-field derivation loop
|
|
230
|
+
(`withCredentialAccess` in `derive-auth-lists.ts`), against a registry built
|
|
231
|
+
by `buildCredentialFieldRegistry` — the stack-seeded `CREDENTIAL_FIELDS` table
|
|
232
|
+
above merged with an app's `authPlugin({ credentialFields })` — keyed by
|
|
233
|
+
better-auth's own model/field key, not the app's list key or column `db.map`,
|
|
234
|
+
so it survives a `modelName` remap or a `fields` column override either way.
|
|
235
|
+
`credentialFields` (`Record<better-auth model key, field key[]>`) is how an
|
|
236
|
+
app marks a credential field on a better-auth plugin the stack doesn't seed a
|
|
237
|
+
set for; it is **additive only** — it can add fields to any model, including a
|
|
238
|
+
seeded one, but can never unmark a seeded field. An entry naming a field
|
|
239
|
+
absent from a model the app actually derives (that plugin is registered)
|
|
240
|
+
throws, naming the model and field; an entry for a model the app doesn't
|
|
241
|
+
derive at all is a silent no-op:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
authPlugin({
|
|
245
|
+
betterAuthPlugins: [passkey()],
|
|
246
|
+
credentialFields: { passkey: ['publicKey'] },
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`sudo()` still reads every one of these fields either way, unaffected: this
|
|
251
|
+
is the supported path for a genuine need (an admin tool, or an app's own auth
|
|
252
|
+
code verifying a password hash via `HashedPassword.compare()`). See
|
|
253
|
+
`packages/core/CLAUDE.md`'s "Access Control Execution Flow" for how a
|
|
254
|
+
field-level `read` denial is enforced, and ADR-0036 for why the rule is about
|
|
255
|
+
whether reading the value confers a live credential, not, say,
|
|
256
|
+
`Account.providerId` or `Session.ipAddress` (identifying, not authenticating
|
|
257
|
+
— left open) — and it does not condition on better-auth's own storage mode
|
|
258
|
+
(`storeTokens`/`storeClientSecret`) or `returned: false` flag, which is
|
|
259
|
+
neither a reliable nor a complete signal (see ADR-0036).
|
|
142
260
|
|
|
143
261
|
### Schema placement (relocatable Auth lists)
|
|
144
262
|
|
package/dist/client/index.d.ts
CHANGED
|
@@ -18,21 +18,5 @@ import type { Session } from 'better-auth/types';
|
|
|
18
18
|
export declare function createClient(options: {
|
|
19
19
|
baseURL: string;
|
|
20
20
|
}): ReturnType<typeof createAuthClient>;
|
|
21
|
-
/**
|
|
22
|
-
* Re-export useful types from better-auth
|
|
23
|
-
*/
|
|
24
21
|
export type { Session };
|
|
25
|
-
/**
|
|
26
|
-
* Note: React hooks (useSession, etc.) are accessed from the client instance
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```typescript
|
|
30
|
-
* import { authClient } from '@/lib/auth-client'
|
|
31
|
-
*
|
|
32
|
-
* function MyComponent() {
|
|
33
|
-
* const { data: session } = authClient.useSession()
|
|
34
|
-
* // ...
|
|
35
|
-
* }
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAI9F;AAED
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAI9F;AAED,YAAY,EAAE,OAAO,EAAE,CAAA"}
|
|
@@ -1,49 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* "Adopt existing better-auth tables" recipe
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `AuthVerification`, mapped into a separate `auth` Postgres schema, and its
|
|
7
|
-
* application `User` (`public.User`) is a *different* model. Reconstructing the
|
|
8
|
-
* matching {@link AuthConfig} by hand — four `modelName`s plus a `schema` on each
|
|
9
|
-
* model — is repetitive and easy to get wrong.
|
|
10
|
-
*
|
|
11
|
-
* {@link adoptBetterAuthTables} produces that {@link AuthConfig} fragment from a
|
|
12
|
-
* couple of options so the migrator doesn't rebuild it from scratch. It only
|
|
13
|
-
* sets the *adoption* knobs (per-model `modelName` + the plugin-level `schema`);
|
|
14
|
-
* the developer composes it with the rest of their auth config (providers,
|
|
15
|
-
* session fields, `extendUserList`, etc.):
|
|
16
|
-
*
|
|
17
|
-
* ```typescript
|
|
18
|
-
* authPlugin({
|
|
19
|
-
* ...adoptBetterAuthTables(),
|
|
20
|
-
* emailAndPassword: { enabled: true },
|
|
21
|
-
* sessionFields: ['userId', 'email', 'name'],
|
|
22
|
-
* })
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* Combined with the keys/field derivation (`deriveAuthLists`) and schema
|
|
26
|
-
* placement, the generated Auth lists reach **Schema parity** with the live
|
|
27
|
-
* tables — they are modelled for runtime/types without producing a destructive
|
|
28
|
-
* auth migration. The recipe never touches the application's own domain `User`:
|
|
29
|
-
* its model names are `Auth`-prefixed by default and the plugin only ever
|
|
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
|
-
* ```
|
|
2
|
+
* "Adopt existing better-auth tables" recipe: produces the {@link AuthConfig}
|
|
3
|
+
* adoption fragment for a pre-existing better-auth install. See
|
|
4
|
+
* `packages/auth/CLAUDE.md` ("Adopting an existing better-auth install") for
|
|
5
|
+
* the full rationale and examples.
|
|
47
6
|
*/
|
|
48
7
|
import type { AuthConfig } from './types.js';
|
|
49
8
|
/**
|
|
@@ -165,10 +124,6 @@ export type AdoptBetterAuthTablesConfig = Pick<AuthConfig, 'schema' | 'user' | '
|
|
|
165
124
|
*
|
|
166
125
|
* Returns only the model/schema knobs needed to adopt the live tables; spread it
|
|
167
126
|
* into {@link authPlugin} alongside your own auth config.
|
|
168
|
-
*
|
|
169
|
-
* @param options - Adoption conventions (schema, model-name prefix, column maps)
|
|
170
|
-
* @returns An {@link AuthConfig} fragment with `schema` + per-model `modelName`
|
|
171
|
-
* (and any field column maps) set to match the live tables
|
|
172
127
|
*/
|
|
173
128
|
export declare function adoptBetterAuthTables(options?: AdoptBetterAuthTablesOptions): AdoptBetterAuthTablesConfig;
|
|
174
129
|
//# sourceMappingURL=adopt-better-auth-tables.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adopt-better-auth-tables.d.ts","sourceRoot":"","sources":["../../src/config/adopt-better-auth-tables.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"adopt-better-auth-tables.d.ts","sourceRoot":"","sources":["../../src/config/adopt-better-auth-tables.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;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;QACrC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KACnC,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;QACrB,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,CAAA;IAED;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAkBD;;;;;;GAMG;AACH,MAAM,MAAM,2BAA2B,GAAG,IAAI,CAC5C,UAAU,EACV,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,CAC3D,GAAG;IACF,SAAS,CAAC,EAAE,UAAU,CAAC,WAAW,CAAC,CAAA;CACpC,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,2BAA2B,CAqC7B"}
|
|
@@ -1,51 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* "Adopt existing better-auth tables" recipe
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `AuthVerification`, mapped into a separate `auth` Postgres schema, and its
|
|
7
|
-
* application `User` (`public.User`) is a *different* model. Reconstructing the
|
|
8
|
-
* matching {@link AuthConfig} by hand — four `modelName`s plus a `schema` on each
|
|
9
|
-
* model — is repetitive and easy to get wrong.
|
|
10
|
-
*
|
|
11
|
-
* {@link adoptBetterAuthTables} produces that {@link AuthConfig} fragment from a
|
|
12
|
-
* couple of options so the migrator doesn't rebuild it from scratch. It only
|
|
13
|
-
* sets the *adoption* knobs (per-model `modelName` + the plugin-level `schema`);
|
|
14
|
-
* the developer composes it with the rest of their auth config (providers,
|
|
15
|
-
* session fields, `extendUserList`, etc.):
|
|
16
|
-
*
|
|
17
|
-
* ```typescript
|
|
18
|
-
* authPlugin({
|
|
19
|
-
* ...adoptBetterAuthTables(),
|
|
20
|
-
* emailAndPassword: { enabled: true },
|
|
21
|
-
* sessionFields: ['userId', 'email', 'name'],
|
|
22
|
-
* })
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* Combined with the keys/field derivation (`deriveAuthLists`) and schema
|
|
26
|
-
* placement, the generated Auth lists reach **Schema parity** with the live
|
|
27
|
-
* tables — they are modelled for runtime/types without producing a destructive
|
|
28
|
-
* auth migration. The recipe never touches the application's own domain `User`:
|
|
29
|
-
* its model names are `Auth`-prefixed by default and the plugin only ever
|
|
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
|
-
* ```
|
|
2
|
+
* "Adopt existing better-auth tables" recipe: produces the {@link AuthConfig}
|
|
3
|
+
* adoption fragment for a pre-existing better-auth install. See
|
|
4
|
+
* `packages/auth/CLAUDE.md` ("Adopting an existing better-auth install") for
|
|
5
|
+
* the full rationale and examples.
|
|
47
6
|
*/
|
|
48
|
-
/** The better-auth models and their default (unprefixed) model names. */
|
|
49
7
|
const MODEL_DEFAULT_NAMES = {
|
|
50
8
|
user: 'User',
|
|
51
9
|
session: 'Session',
|
|
@@ -53,7 +11,6 @@ const MODEL_DEFAULT_NAMES = {
|
|
|
53
11
|
verification: 'Verification',
|
|
54
12
|
rateLimit: 'RateLimit',
|
|
55
13
|
};
|
|
56
|
-
/** better-auth's own default lowercase table names, per model. */
|
|
57
14
|
const BETTER_AUTH_DEFAULT_TABLE_NAMES = {
|
|
58
15
|
user: 'user',
|
|
59
16
|
session: 'session',
|
|
@@ -67,10 +24,6 @@ const BETTER_AUTH_DEFAULT_TABLE_NAMES = {
|
|
|
67
24
|
*
|
|
68
25
|
* Returns only the model/schema knobs needed to adopt the live tables; spread it
|
|
69
26
|
* into {@link authPlugin} alongside your own auth config.
|
|
70
|
-
*
|
|
71
|
-
* @param options - Adoption conventions (schema, model-name prefix, column maps)
|
|
72
|
-
* @returns An {@link AuthConfig} fragment with `schema` + per-model `modelName`
|
|
73
|
-
* (and any field column maps) set to match the live tables
|
|
74
27
|
*/
|
|
75
28
|
export function adoptBetterAuthTables(options = {}) {
|
|
76
29
|
const { schema = 'auth', modelNamePrefix = 'Auth', fields = {}, useBetterAuthTableNames = false, tableNames = {}, rateLimit = false, } = options;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adopt-better-auth-tables.js","sourceRoot":"","sources":["../../src/config/adopt-better-auth-tables.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"adopt-better-auth-tables.js","sourceRoot":"","sources":["../../src/config/adopt-better-auth-tables.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiHH,MAAM,mBAAmB,GAAG;IAC1B,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,cAAc;IAC5B,SAAS,EAAE,WAAW;CACd,CAAA;AAEV,MAAM,+BAA+B,GAAG;IACtC,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,cAAc;IAC5B,SAAS,EAAE,WAAW;CACd,CAAA;AAgBV;;;;;;GAMG;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,EACf,SAAS,GAAG,KAAK,GAClB,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;QACxC,GAAG,CAAC,SAAS;YACX,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,UAAmB,EAAE,GAAG,UAAU,CAAC,WAAW,CAAC,EAAE,EAAE;YAC5F,CAAC,CAAC,EAAE,CAAC;KACR,CAAA;AACH,CAAC"}
|