@opensaas/stack-auth 0.39.2 → 0.40.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 +151 -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/src/config/index.ts
CHANGED
|
@@ -10,12 +10,6 @@ import type {
|
|
|
10
10
|
SendAuthEmail,
|
|
11
11
|
} from './types.js'
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* Default better-auth model names. Used when the developer does not override
|
|
15
|
-
* `modelName`, preserving the historical `User`/`Session`/`Account`/`Verification`
|
|
16
|
-
* keys exactly. `rateLimit` only applies when `rateLimit.storage: 'database'`
|
|
17
|
-
* derives the fifth Auth list.
|
|
18
|
-
*/
|
|
19
13
|
const DEFAULT_MODEL_NAMES = {
|
|
20
14
|
user: 'User',
|
|
21
15
|
session: 'Session',
|
|
@@ -25,11 +19,6 @@ const DEFAULT_MODEL_NAMES = {
|
|
|
25
19
|
} as const
|
|
26
20
|
|
|
27
21
|
/**
|
|
28
|
-
* Resolve a single better-auth model config block into its normalized form,
|
|
29
|
-
* falling back to the better-auth default model name and an empty column map.
|
|
30
|
-
* The model's Postgres schema is the per-model `schema` override when present,
|
|
31
|
-
* otherwise the plugin-level `schema` default (or `undefined` for `public`).
|
|
32
|
-
*
|
|
33
22
|
* `tableName` defaults to today's behaviour when not explicitly set: it
|
|
34
23
|
* follows `modelName` when that differs from the better-auth default (so a
|
|
35
24
|
* renamed list still pins its table via `@@map`), otherwise it stays unset.
|
|
@@ -49,15 +38,10 @@ function normalizeModelConfig(
|
|
|
49
38
|
tableName,
|
|
50
39
|
fields: config?.fields || {},
|
|
51
40
|
schema: config?.schema ?? defaultSchema,
|
|
41
|
+
indexes: config?.indexes ?? [],
|
|
52
42
|
}
|
|
53
43
|
}
|
|
54
44
|
|
|
55
|
-
/**
|
|
56
|
-
* Resolve the better-auth model config for all four auth models, plus a fifth
|
|
57
|
-
* `rateLimit` model when `rateLimit.storage: 'database'` requires deriving
|
|
58
|
-
* the `RateLimit` list. `defaultSchema` is the plugin-level `schema` applied
|
|
59
|
-
* to every model unless a per-model `schema` override is given.
|
|
60
|
-
*/
|
|
61
45
|
function normalizeAuthModels(config: AuthConfig): NormalizedAuthModels {
|
|
62
46
|
const defaultSchema = config.schema
|
|
63
47
|
const models: NormalizedAuthModels = {
|
|
@@ -82,11 +66,6 @@ function normalizeAuthModels(config: AuthConfig): NormalizedAuthModels {
|
|
|
82
66
|
return models
|
|
83
67
|
}
|
|
84
68
|
|
|
85
|
-
/**
|
|
86
|
-
* Default `sendResetPassword`/`sendVerificationEmail` — logs to console
|
|
87
|
-
* instead of sending, matching the pre-existing "no sendEmail configured"
|
|
88
|
-
* behavior for apps that haven't wired a real email provider yet.
|
|
89
|
-
*/
|
|
90
69
|
function defaultSendAuthEmail(kind: 'password reset' | 'verification'): SendAuthEmail {
|
|
91
70
|
return async ({ user, url }) => {
|
|
92
71
|
console.log(
|
|
@@ -97,11 +76,7 @@ function defaultSendAuthEmail(kind: 'password reset' | 'verification'): SendAuth
|
|
|
97
76
|
}
|
|
98
77
|
}
|
|
99
78
|
|
|
100
|
-
/**
|
|
101
|
-
* Normalize auth configuration with defaults
|
|
102
|
-
*/
|
|
103
79
|
export function normalizeAuthConfig(config: AuthConfig): NormalizedAuthConfig {
|
|
104
|
-
// Email and password defaults
|
|
105
80
|
const emailAndPassword = config.emailAndPassword?.enabled
|
|
106
81
|
? {
|
|
107
82
|
enabled: true as const,
|
|
@@ -119,7 +94,6 @@ export function normalizeAuthConfig(config: AuthConfig): NormalizedAuthConfig {
|
|
|
119
94
|
sendResetPassword: defaultSendAuthEmail('password reset'),
|
|
120
95
|
}
|
|
121
96
|
|
|
122
|
-
// Email verification defaults
|
|
123
97
|
const emailVerification = config.emailVerification?.enabled
|
|
124
98
|
? {
|
|
125
99
|
enabled: true as const,
|
|
@@ -137,7 +111,6 @@ export function normalizeAuthConfig(config: AuthConfig): NormalizedAuthConfig {
|
|
|
137
111
|
sendVerificationEmail: defaultSendAuthEmail('verification'),
|
|
138
112
|
}
|
|
139
113
|
|
|
140
|
-
// Password reset defaults
|
|
141
114
|
const passwordReset = config.passwordReset?.enabled
|
|
142
115
|
? {
|
|
143
116
|
enabled: true as const,
|
|
@@ -145,17 +118,13 @@ export function normalizeAuthConfig(config: AuthConfig): NormalizedAuthConfig {
|
|
|
145
118
|
}
|
|
146
119
|
: { enabled: false as const, tokenExpiration: 3600 }
|
|
147
120
|
|
|
148
|
-
// Session defaults
|
|
149
121
|
const session = {
|
|
150
|
-
expiresIn: config.session?.expiresIn || 604800,
|
|
151
|
-
updateAge: config.session?.updateAge ?? 86400, //
|
|
122
|
+
expiresIn: config.session?.expiresIn || 604800,
|
|
123
|
+
updateAge: config.session?.updateAge ?? 86400, // matches better-auth's own default
|
|
152
124
|
}
|
|
153
125
|
|
|
154
|
-
// Session fields defaults
|
|
155
126
|
const sessionFields = config.sessionFields || ['userId', 'email', 'name']
|
|
156
127
|
|
|
157
|
-
// Resolve better-auth per-model config (modelName + field column maps).
|
|
158
|
-
// Defaults preserve the historical User/Session/Account/Verification keys.
|
|
159
128
|
const models = normalizeAuthModels(config)
|
|
160
129
|
|
|
161
130
|
return {
|
|
@@ -169,6 +138,7 @@ export function normalizeAuthConfig(config: AuthConfig): NormalizedAuthConfig {
|
|
|
169
138
|
sessionFields,
|
|
170
139
|
extendUserList: config.extendUserList || {},
|
|
171
140
|
access: config.access || {},
|
|
141
|
+
credentialFields: config.credentialFields || {},
|
|
172
142
|
betterAuthPlugins: config.betterAuthPlugins || [],
|
|
173
143
|
rateLimit: config.rateLimit,
|
|
174
144
|
betterAuthOptions: config.betterAuthOptions || {},
|
package/src/config/plugin.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { getDbKey } from '@opensaas/stack-core'
|
|
|
3
3
|
import type { AuthConfig, NormalizedAuthConfig } from './types.js'
|
|
4
4
|
import { normalizeAuthConfig } from './index.js'
|
|
5
5
|
import { getAuthLists } from '../lists/index.js'
|
|
6
|
-
import { convertBetterAuthSchema } from '../server/schema-converter.js'
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Auth plugin for OpenSaas Stack
|
|
@@ -39,49 +38,29 @@ export function authPlugin(config: AuthConfig): Plugin {
|
|
|
39
38
|
},
|
|
40
39
|
|
|
41
40
|
init: async (context) => {
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
41
|
+
// One consolidated derivation covers the four base models, the
|
|
42
|
+
// optional RateLimit model, and every table a better-auth plugin
|
|
43
|
+
// declares in its own `schema` (base-model extensions like the
|
|
44
|
+
// `anonymous` plugin's `user.isAnonymous`, and standalone tables like
|
|
45
|
+
// the MCP plugin's OAuth tables) — see `deriveAuthLists` (issue #992).
|
|
46
|
+
// `getAuthTables` merges a plugin's base-model schema extension
|
|
47
|
+
// directly into that model's own fields, so the derived `User`/`AuthUser`
|
|
48
|
+
// list here already carries e.g. `isAnonymous` before this loop runs.
|
|
47
49
|
const authLists = getAuthLists(
|
|
48
50
|
normalized.extendUserList,
|
|
49
51
|
normalized.models,
|
|
50
52
|
normalized.access,
|
|
53
|
+
normalized.betterAuthPlugins,
|
|
54
|
+
normalized.credentialFields,
|
|
51
55
|
)
|
|
52
56
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
const baseModelKeys = {
|
|
61
|
-
user: normalized.models.user.modelName,
|
|
62
|
-
session: normalized.models.session.modelName,
|
|
63
|
-
account: normalized.models.account.modelName,
|
|
64
|
-
verification: normalized.models.verification.modelName,
|
|
65
|
-
...(normalized.models.rateLimit
|
|
66
|
-
? { rateLimit: normalized.models.rateLimit.modelName }
|
|
67
|
-
: {}),
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Add all auth lists FIRST, before any better-auth plugin schema
|
|
71
|
-
// extension is processed. This must happen before the betterAuthPlugins
|
|
72
|
-
// loop below so a base-model extension (e.g. a plugin's `schema: {
|
|
73
|
-
// user: { fields: … } }`) always finds the real derived list already
|
|
74
|
-
// registered under its key and takes the merge (`extendList`) path
|
|
75
|
-
// against it — instead of pre-empting that key with a bare
|
|
76
|
-
// `list({ fields })` that carries no `db`/`access` (see #861).
|
|
77
|
-
//
|
|
78
|
-
// The plugin only ever touches its OWN derived keys. When a developer
|
|
79
|
-
// renames the auth user model (e.g. user.modelName: 'AuthUser'), the
|
|
80
|
-
// derived key is 'AuthUser' and an app's separate 'User' list is left
|
|
81
|
-
// untouched — the plugin never extends/overwrites a list it didn't
|
|
82
|
-
// derive. Extending only kicks in when an existing list shares the
|
|
83
|
-
// derived key (e.g. the default 'User'), which is the intended
|
|
84
|
-
// "merge auth fields into my User" behaviour.
|
|
57
|
+
// Base models are always the first entries in `authLists` (see
|
|
58
|
+
// `deriveAuthLists`), so a plugin table's reverse relation onto a base
|
|
59
|
+
// model (e.g. `AuthUser.oauthApplications`) is already part of that
|
|
60
|
+
// base list's `fields` by the time this loop reaches it — one pass
|
|
61
|
+
// suffices for both: a list already declared by the app (or added by
|
|
62
|
+
// an earlier iteration of this same loop) merges via `extendList`;
|
|
63
|
+
// everything else registers via `addList`.
|
|
85
64
|
for (const [listName, listConfig] of Object.entries(authLists)) {
|
|
86
65
|
if (context.config.lists[listName]) {
|
|
87
66
|
// A list already exists under this derived key — merge auth fields
|
|
@@ -94,55 +73,14 @@ export function authPlugin(config: AuthConfig): Plugin {
|
|
|
94
73
|
mcp: listConfig.mcp,
|
|
95
74
|
})
|
|
96
75
|
} else {
|
|
97
|
-
// Otherwise, add the auth list
|
|
98
76
|
context.addList(listName, listConfig)
|
|
99
77
|
}
|
|
100
78
|
}
|
|
101
79
|
|
|
102
|
-
// Extract additional lists from Better Auth plugins. Because the auth
|
|
103
|
-
// lists above are already registered, a schema extension of a base
|
|
104
|
-
// model (user/session/account/verification) always finds its resolved
|
|
105
|
-
// key occupied by the real derived list and merges via `extendList` —
|
|
106
|
-
// its `db`/`access` are preserved. Non-base plugin tables (e.g.
|
|
107
|
-
// `oauth_application`, `passkey`) still register as new lists via
|
|
108
|
-
// `addList`, same as before.
|
|
109
|
-
for (const plugin of normalized.betterAuthPlugins) {
|
|
110
|
-
if (plugin && typeof plugin === 'object' && plugin.schema) {
|
|
111
|
-
// Plugin has schema property - convert to OpenSaaS lists
|
|
112
|
-
const pluginSchema = plugin.schema
|
|
113
|
-
const pluginLists = convertBetterAuthSchema(pluginSchema, baseModelKeys)
|
|
114
|
-
|
|
115
|
-
// Add or extend lists from plugin
|
|
116
|
-
for (const [listName, listConfig] of Object.entries(pluginLists)) {
|
|
117
|
-
if (context.config.lists[listName]) {
|
|
118
|
-
// List already exists — merge fields/hooks/mcp in only. Access
|
|
119
|
-
// control belongs to whoever owns the list; per ADR-0013 an
|
|
120
|
-
// extension must never carry operation-level access for a
|
|
121
|
-
// pre-existing list (the plugin engine throws if it does).
|
|
122
|
-
context.extendList(listName, {
|
|
123
|
-
fields: listConfig.fields,
|
|
124
|
-
hooks: listConfig.hooks,
|
|
125
|
-
mcp: listConfig.mcp,
|
|
126
|
-
})
|
|
127
|
-
} else {
|
|
128
|
-
// List doesn't exist, add it
|
|
129
|
-
context.addList(listName, listConfig)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Store auth config for runtime access
|
|
136
|
-
// Access at runtime via: config._pluginData.auth
|
|
137
80
|
context.setPluginData<NormalizedAuthConfig>('auth', normalized)
|
|
138
81
|
},
|
|
139
82
|
|
|
140
83
|
beforeGenerate: (generationConfig) => {
|
|
141
|
-
// Collect every schema the Auth lists are placed in (per-model schema,
|
|
142
|
-
// else the plugin-level schema). When none is configured the Auth lists
|
|
143
|
-
// stay in the default `public` schema and we leave the config untouched —
|
|
144
|
-
// the greenfield default Prisma schema is unchanged (no `schemas`, no
|
|
145
|
-
// `previewFeatures`, no `@@schema`).
|
|
146
84
|
const authSchemas = Array.from(
|
|
147
85
|
new Set(
|
|
148
86
|
Object.values(normalized.models)
|
|
@@ -181,18 +119,13 @@ export function authPlugin(config: AuthConfig): Plugin {
|
|
|
181
119
|
},
|
|
182
120
|
|
|
183
121
|
runtime: (context, sudo) => {
|
|
184
|
-
// Resolve the user list's context.db key from the configured user model.
|
|
185
|
-
// context.db is keyed camelCase, so 'User' -> 'user', 'AuthUser' -> 'authUser'.
|
|
186
122
|
const userDbKey = getDbKey(normalized.models.user.modelName)
|
|
187
123
|
|
|
188
|
-
// Provide auth-related utilities at runtime
|
|
189
124
|
return {
|
|
190
125
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* by default, and "who is this session" must not depend on the
|
|
195
|
-
* application's User access policy.
|
|
126
|
+
* Resolves through `sudo()` (per ADR-0013): the User list ships
|
|
127
|
+
* closed by default, and "who is this session" must not depend on
|
|
128
|
+
* the application's User access policy.
|
|
196
129
|
*/
|
|
197
130
|
getUser: async (userId: string) => {
|
|
198
131
|
return await sudo().db[userDbKey].findUnique({
|
|
@@ -200,10 +133,7 @@ export function authPlugin(config: AuthConfig): Plugin {
|
|
|
200
133
|
})
|
|
201
134
|
},
|
|
202
135
|
|
|
203
|
-
/**
|
|
204
|
-
* Get current user from session. Extracts userId from session and
|
|
205
|
-
* fetches user data via `sudo()` — see {@link getUser}.
|
|
206
|
-
*/
|
|
136
|
+
/** See {@link getUser} — same `sudo()` rationale (ADR-0013), keyed off `context.session.userId`. */
|
|
207
137
|
getCurrentUser: async () => {
|
|
208
138
|
if (!context.session?.userId) {
|
|
209
139
|
return null
|
package/src/config/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ListConfig } from '@opensaas/stack-core'
|
|
1
|
+
import type { ListConfig, ListIndex } from '@opensaas/stack-core'
|
|
2
2
|
import type { BetterAuthOptions, BetterAuthPlugin, User } from 'better-auth'
|
|
3
3
|
import type { ExtendUserListConfig } from '../lists/index.js'
|
|
4
4
|
|
|
@@ -13,18 +13,12 @@ export type SendAuthEmail = (
|
|
|
13
13
|
request?: Request,
|
|
14
14
|
) => Promise<void>
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* OAuth provider configuration
|
|
18
|
-
*/
|
|
19
16
|
export type OAuthProvider = {
|
|
20
17
|
clientId: string
|
|
21
18
|
clientSecret: string
|
|
22
19
|
enabled?: boolean
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
/**
|
|
26
|
-
* Social provider configurations
|
|
27
|
-
*/
|
|
28
22
|
export type SocialProvidersConfig = {
|
|
29
23
|
github?: OAuthProvider
|
|
30
24
|
google?: OAuthProvider
|
|
@@ -33,9 +27,6 @@ export type SocialProvidersConfig = {
|
|
|
33
27
|
[key: string]: OAuthProvider | undefined
|
|
34
28
|
}
|
|
35
29
|
|
|
36
|
-
/**
|
|
37
|
-
* Email and password configuration
|
|
38
|
-
*/
|
|
39
30
|
export type EmailPasswordConfig = {
|
|
40
31
|
enabled: boolean
|
|
41
32
|
/**
|
|
@@ -76,9 +67,6 @@ export type EmailPasswordConfig = {
|
|
|
76
67
|
sendResetPassword?: SendAuthEmail
|
|
77
68
|
}
|
|
78
69
|
|
|
79
|
-
/**
|
|
80
|
-
* Email verification configuration
|
|
81
|
-
*/
|
|
82
70
|
export type EmailVerificationConfig = {
|
|
83
71
|
enabled: boolean
|
|
84
72
|
/**
|
|
@@ -111,9 +99,6 @@ export type EmailVerificationConfig = {
|
|
|
111
99
|
sendVerificationEmail?: SendAuthEmail
|
|
112
100
|
}
|
|
113
101
|
|
|
114
|
-
/**
|
|
115
|
-
* Password reset configuration
|
|
116
|
-
*/
|
|
117
102
|
export type PasswordResetConfig = {
|
|
118
103
|
enabled: boolean
|
|
119
104
|
/**
|
|
@@ -123,9 +108,6 @@ export type PasswordResetConfig = {
|
|
|
123
108
|
tokenExpiration?: number
|
|
124
109
|
}
|
|
125
110
|
|
|
126
|
-
/**
|
|
127
|
-
* Session configuration
|
|
128
|
-
*/
|
|
129
111
|
export type SessionConfig = {
|
|
130
112
|
/**
|
|
131
113
|
* Session expiration in seconds
|
|
@@ -141,24 +123,6 @@ export type SessionConfig = {
|
|
|
141
123
|
updateAge?: number | false
|
|
142
124
|
}
|
|
143
125
|
|
|
144
|
-
/**
|
|
145
|
-
* Per-model better-auth configuration block.
|
|
146
|
-
*
|
|
147
|
-
* Mirrors better-auth's own `BetterAuthDBOptions` (the `user`/`session`/
|
|
148
|
-
* `account`/`verification` config a developer already writes): `modelName`
|
|
149
|
-
* renames the table/list and `fields` maps individual better-auth field names
|
|
150
|
-
* to database column names. The auth plugin derives its Auth lists from this
|
|
151
|
-
* config so the generated lists carry the same keys and column maps as the
|
|
152
|
-
* developer's live better-auth tables.
|
|
153
|
-
*
|
|
154
|
-
* @example
|
|
155
|
-
* ```typescript
|
|
156
|
-
* authPlugin({
|
|
157
|
-
* user: { modelName: 'AuthUser', fields: { name: 'full_name' } },
|
|
158
|
-
* session: { modelName: 'AuthSession' },
|
|
159
|
-
* })
|
|
160
|
-
* ```
|
|
161
|
-
*/
|
|
162
126
|
/**
|
|
163
127
|
* App-authored operation + field-level access control for the Auth lists,
|
|
164
128
|
* keyed by better-auth model name (not by the derived list key, so it stays
|
|
@@ -213,6 +177,24 @@ export type AuthAccessConfig = {
|
|
|
213
177
|
rateLimit?: ListConfig<any>['access']
|
|
214
178
|
}
|
|
215
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Per-model better-auth configuration block.
|
|
182
|
+
*
|
|
183
|
+
* Mirrors better-auth's own `BetterAuthDBOptions` (the `user`/`session`/
|
|
184
|
+
* `account`/`verification` config a developer already writes): `modelName`
|
|
185
|
+
* renames the table/list and `fields` maps individual better-auth field names
|
|
186
|
+
* to database column names. The auth plugin derives its Auth lists from this
|
|
187
|
+
* config so the generated lists carry the same keys and column maps as the
|
|
188
|
+
* developer's live better-auth tables.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```typescript
|
|
192
|
+
* authPlugin({
|
|
193
|
+
* user: { modelName: 'AuthUser', fields: { name: 'full_name' } },
|
|
194
|
+
* session: { modelName: 'AuthSession' },
|
|
195
|
+
* })
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
216
198
|
export type AuthModelConfig = {
|
|
217
199
|
/**
|
|
218
200
|
* The table/list name for this model.
|
|
@@ -262,35 +244,50 @@ export type AuthModelConfig = {
|
|
|
262
244
|
* ```
|
|
263
245
|
*/
|
|
264
246
|
schema?: string
|
|
247
|
+
/**
|
|
248
|
+
* App-authored model-level `@@unique`/`@@index` constraints for this auth
|
|
249
|
+
* model, in the same shape as a list's own {@link ListConfig.db} `indexes`
|
|
250
|
+
* (core's {@link ListIndex}). Entries name this model's own OpenSaaS field
|
|
251
|
+
* keys (e.g. `identifier`, `createdAt` on `verification`) — the same names
|
|
252
|
+
* used in this model's `fields` column map — not raw database column names.
|
|
253
|
+
*
|
|
254
|
+
* The stack already derives some indexes from better-auth's own table
|
|
255
|
+
* definitions (e.g. `User.email` is `@unique`). When an entry here covers a
|
|
256
|
+
* column that also carries a derived index, the derived index is
|
|
257
|
+
* suppressed for that column and only this entry is emitted — the
|
|
258
|
+
* application's declaration wins (ADR-0035). This is what makes adopting a
|
|
259
|
+
* live database's real constraint name, or extending a derived column into
|
|
260
|
+
* a composite index, expressible.
|
|
261
|
+
*
|
|
262
|
+
* @example Adopt a live `user.email` unique constraint under its real name
|
|
263
|
+
* ```typescript
|
|
264
|
+
* authPlugin({
|
|
265
|
+
* user: { indexes: [{ fields: ['email'], unique: true, name: 'user_email_key' }] },
|
|
266
|
+
* })
|
|
267
|
+
* ```
|
|
268
|
+
*
|
|
269
|
+
* @example Extend a derived index into a composite (per-identifier resend cooldown)
|
|
270
|
+
* ```typescript
|
|
271
|
+
* authPlugin({
|
|
272
|
+
* verification: {
|
|
273
|
+
* indexes: [{ fields: ['identifier', { field: 'createdAt', sort: 'desc' }] }],
|
|
274
|
+
* },
|
|
275
|
+
* })
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
indexes?: ListIndex[]
|
|
265
279
|
}
|
|
266
280
|
|
|
267
|
-
/**
|
|
268
|
-
* Auth configuration options
|
|
269
|
-
*/
|
|
270
281
|
export type AuthConfig = {
|
|
271
|
-
/**
|
|
272
|
-
* Email and password authentication
|
|
273
|
-
*/
|
|
274
282
|
emailAndPassword?: EmailPasswordConfig | { enabled: true }
|
|
275
283
|
|
|
276
|
-
/**
|
|
277
|
-
* Email verification
|
|
278
|
-
*/
|
|
279
284
|
emailVerification?: EmailVerificationConfig | { enabled: true }
|
|
280
285
|
|
|
281
|
-
/**
|
|
282
|
-
* Password reset
|
|
283
|
-
*/
|
|
284
286
|
passwordReset?: PasswordResetConfig | { enabled: true }
|
|
285
287
|
|
|
286
|
-
/**
|
|
287
|
-
* OAuth/social providers
|
|
288
|
-
*/
|
|
289
288
|
socialProviders?: SocialProvidersConfig
|
|
290
289
|
|
|
291
290
|
/**
|
|
292
|
-
* Session configuration.
|
|
293
|
-
*
|
|
294
291
|
* Carries session expiry settings as well as the better-auth `session` model
|
|
295
292
|
* config (`modelName` + field column `fields` maps) used to derive the Auth
|
|
296
293
|
* session list.
|
|
@@ -305,14 +302,8 @@ export type AuthConfig = {
|
|
|
305
302
|
*/
|
|
306
303
|
user?: AuthModelConfig
|
|
307
304
|
|
|
308
|
-
/**
|
|
309
|
-
* better-auth `account` model configuration (modelName + field column maps).
|
|
310
|
-
*/
|
|
311
305
|
account?: AuthModelConfig
|
|
312
306
|
|
|
313
|
-
/**
|
|
314
|
-
* better-auth `verification` model configuration (modelName + field column maps).
|
|
315
|
-
*/
|
|
316
307
|
verification?: AuthModelConfig
|
|
317
308
|
|
|
318
309
|
/**
|
|
@@ -402,16 +393,51 @@ export type AuthConfig = {
|
|
|
402
393
|
*/
|
|
403
394
|
access?: AuthAccessConfig
|
|
404
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Mark additional better-auth model fields as credentials, so they ship
|
|
398
|
+
* field-level read-denied like the stack's own seeded set (ADR-0036) — the
|
|
399
|
+
* six base-model fields plus, as of issue #1014, the `mcp`/oauth-provider
|
|
400
|
+
* plugin's `oauthClient.clientSecret`/`oauthAccessToken.token`/
|
|
401
|
+
* `oauthRefreshToken.token` and `twoFactor()`'s `twoFactor.secret`/
|
|
402
|
+
* `backupCodes`. Use this for a better-auth plugin the stack doesn't seed a
|
|
403
|
+
* credential set for.
|
|
404
|
+
*
|
|
405
|
+
* Keyed by better-auth's own **model key** (e.g. `'twoFactor'`,
|
|
406
|
+
* `'oauthClient'` — not the derived list key, so it stays remap-proof), each
|
|
407
|
+
* naming better-auth **field keys** (not mapped column names).
|
|
408
|
+
*
|
|
409
|
+
* Strictly additive: an entry can mark further fields as credentials, but
|
|
410
|
+
* can never unmark one of the stack's seeded fields — omitting or emptying
|
|
411
|
+
* a seeded model's list here leaves that model's seeded deny standing. An
|
|
412
|
+
* entry naming a field that doesn't exist on a model the app actually
|
|
413
|
+
* derives (has that plugin registered) throws, naming the model and field;
|
|
414
|
+
* an entry for a model the app doesn't derive at all is a silent no-op.
|
|
415
|
+
* An entry naming an id-referencing relationship field (e.g. an FK ending
|
|
416
|
+
* `Id`) also throws — that field derives to a `relationship()`, never a
|
|
417
|
+
* scalar column, so a deny registered against it could never apply.
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* ```typescript
|
|
421
|
+
* authPlugin({
|
|
422
|
+
* betterAuthPlugins: [passkey()],
|
|
423
|
+
* credentialFields: { passkey: ['publicKey'] },
|
|
424
|
+
* })
|
|
425
|
+
* ```
|
|
426
|
+
*/
|
|
427
|
+
credentialFields?: Record<string, string[]>
|
|
428
|
+
|
|
405
429
|
/**
|
|
406
430
|
* Additional Better Auth plugins to enable
|
|
407
431
|
* Allows integrating any Better Auth plugin (MCP, 2FA, etc.)
|
|
408
432
|
*
|
|
409
433
|
* @example
|
|
410
434
|
* ```typescript
|
|
411
|
-
* import { mcp } from '
|
|
435
|
+
* import { mcp } from '@opensaas/stack-auth/plugins'
|
|
436
|
+
* import { jwt } from 'better-auth/plugins'
|
|
412
437
|
*
|
|
413
438
|
* betterAuthPlugins: [
|
|
414
|
-
* mcp(
|
|
439
|
+
* jwt(), // required alongside mcp() since better-auth 1.7
|
|
440
|
+
* mcp({ loginPage: '/sign-in', consentPage: '/consent', resource: 'https://your-app.com/api/mcp' })
|
|
415
441
|
* ]
|
|
416
442
|
* ```
|
|
417
443
|
*/
|
|
@@ -530,6 +556,8 @@ export type NormalizedAuthModelConfig = {
|
|
|
530
556
|
tableName?: string
|
|
531
557
|
fields: Record<string, string>
|
|
532
558
|
schema?: string
|
|
559
|
+
/** App-authored `db.indexes` entries for this model (see {@link AuthModelConfig.indexes}). Defaults to `[]`. */
|
|
560
|
+
indexes?: ListIndex[]
|
|
533
561
|
}
|
|
534
562
|
|
|
535
563
|
/**
|
|
@@ -550,10 +578,6 @@ export type NormalizedAuthModels = {
|
|
|
550
578
|
rateLimit?: NormalizedAuthModelConfig
|
|
551
579
|
}
|
|
552
580
|
|
|
553
|
-
/**
|
|
554
|
-
* Internal normalized auth configuration
|
|
555
|
-
* Used after parsing user config
|
|
556
|
-
*/
|
|
557
581
|
export type NormalizedAuthConfig = Required<
|
|
558
582
|
Omit<
|
|
559
583
|
AuthConfig,
|
|
@@ -574,7 +598,6 @@ export type NormalizedAuthConfig = Required<
|
|
|
574
598
|
passwordReset: Required<PasswordResetConfig>
|
|
575
599
|
/** Resolved session expiry settings (model config lives under `models.session`). */
|
|
576
600
|
session: Required<SessionConfig>
|
|
577
|
-
/** Resolved better-auth model config (modelName + field column maps + schema) for all auth models. */
|
|
578
601
|
models: NormalizedAuthModels
|
|
579
602
|
/**
|
|
580
603
|
* Plugin-level Postgres schema for the Auth lists, if any. Resolved per-model
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @opensaas/stack-auth
|
|
3
|
-
*
|
|
4
|
-
* Better-auth integration for OpenSaas Stack
|
|
5
|
-
*
|
|
6
|
-
* This package provides:
|
|
7
|
-
* - Auto-generated User, Session, Account, Verification lists
|
|
8
|
-
* - Session integration with OpenSaas access control
|
|
9
|
-
* - Pre-built auth UI components (SignIn, SignUp, ForgotPassword)
|
|
10
|
-
* - Easy configuration with authPlugin()
|
|
2
|
+
* @opensaas/stack-auth — Better-auth integration for OpenSaas Stack.
|
|
11
3
|
*
|
|
12
4
|
* @example
|
|
13
5
|
* ```typescript
|
|
@@ -28,7 +20,6 @@
|
|
|
28
20
|
* ```
|
|
29
21
|
*/
|
|
30
22
|
|
|
31
|
-
// Config exports
|
|
32
23
|
export { normalizeAuthConfig } from './config/index.js'
|
|
33
24
|
export { authPlugin } from './config/plugin.js'
|
|
34
25
|
export type { AuthConfig, NormalizedAuthConfig } from './config/index.js'
|
|
@@ -38,16 +29,14 @@ export type * from './config/types.js'
|
|
|
38
29
|
export { deriveAuthLists } from './config/derive-auth-lists.js'
|
|
39
30
|
export type { DerivedAuthLists } from './config/derive-auth-lists.js'
|
|
40
31
|
|
|
41
|
-
// "Adopt existing better-auth tables" recipe —
|
|
42
|
-
//
|
|
43
|
-
// project reaches Schema parity without rebuilding the config by hand.
|
|
32
|
+
// "Adopt existing better-auth tables" recipe — presets the model/schema knobs
|
|
33
|
+
// for a pre-existing separate-schema better-auth install (advanced use case).
|
|
44
34
|
export { adoptBetterAuthTables } from './config/adopt-better-auth-tables.js'
|
|
45
35
|
export type {
|
|
46
36
|
AdoptBetterAuthTablesOptions,
|
|
47
37
|
AdoptBetterAuthTablesConfig,
|
|
48
38
|
} from './config/adopt-better-auth-tables.js'
|
|
49
39
|
|
|
50
|
-
// Runtime type exports
|
|
51
40
|
export type { AuthRuntimeServices } from './runtime/types.js'
|
|
52
41
|
|
|
53
42
|
// List generators (for advanced use cases)
|
package/src/lists/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ListConfig, FieldConfig } from '@opensaas/stack-core'
|
|
2
|
+
import type { BetterAuthPlugin } from 'better-auth'
|
|
2
3
|
import type { AuthAccessConfig, NormalizedAuthModels } from '../config/types.js'
|
|
3
4
|
import { deriveAuthLists } from '../config/derive-auth-lists.js'
|
|
4
5
|
|
|
@@ -42,8 +43,6 @@ const DEFAULT_MODELS: NormalizedAuthModels = {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
|
-
* Create the base User list with better-auth required fields.
|
|
46
|
-
*
|
|
47
46
|
* Backwards-compatible helper: derives the default `User` list (keyed `User`,
|
|
48
47
|
* default field shapes) via {@link deriveAuthLists}.
|
|
49
48
|
*/
|
|
@@ -79,7 +78,9 @@ export function createVerificationList(): ListConfig<any> {
|
|
|
79
78
|
}
|
|
80
79
|
|
|
81
80
|
/**
|
|
82
|
-
* Get all auth lists required by better-auth
|
|
81
|
+
* Get all auth lists required by better-auth — the four base models, an
|
|
82
|
+
* optional database-backed `RateLimit`, and any table a better-auth plugin
|
|
83
|
+
* declares in its own `schema` (issue #992).
|
|
83
84
|
*
|
|
84
85
|
* Derives the Auth lists from the resolved better-auth model config. When no
|
|
85
86
|
* `models` are supplied (or none carry overrides), the result is the historical
|
|
@@ -87,13 +88,24 @@ export function createVerificationList(): ListConfig<any> {
|
|
|
87
88
|
*
|
|
88
89
|
* @param userConfig - Extra User-list fields/access/hooks (from `extendUserList`)
|
|
89
90
|
* @param models - Resolved better-auth model config; defaults to the better-auth defaults
|
|
90
|
-
* @param accessConfig - App-authored access for each Auth list, keyed by better-auth model name
|
|
91
|
+
* @param accessConfig - App-authored access for each base Auth list, keyed by better-auth model name
|
|
92
|
+
* @param plugins - The app's better-auth plugins (`authPlugin({ betterAuthPlugins })`)
|
|
93
|
+
* @param credentialFieldsConfig - App-authored additions to the credential-field read-deny
|
|
94
|
+
* (`authPlugin({ credentialFields })`), keyed by better-auth model key
|
|
91
95
|
*/
|
|
92
96
|
export function getAuthLists(
|
|
93
97
|
userConfig?: ExtendUserListConfig,
|
|
94
98
|
models: NormalizedAuthModels = DEFAULT_MODELS,
|
|
95
99
|
accessConfig?: AuthAccessConfig,
|
|
100
|
+
plugins?: BetterAuthPlugin[],
|
|
101
|
+
credentialFieldsConfig?: Record<string, string[]>,
|
|
96
102
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
97
103
|
): Record<string, ListConfig<any>> {
|
|
98
|
-
return deriveAuthLists(
|
|
104
|
+
return deriveAuthLists(
|
|
105
|
+
models,
|
|
106
|
+
userConfig || {},
|
|
107
|
+
accessConfig || {},
|
|
108
|
+
plugins || [],
|
|
109
|
+
credentialFieldsConfig || {},
|
|
110
|
+
).lists
|
|
99
111
|
}
|