@opensaas/stack-auth 0.20.1 → 0.22.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 +122 -0
- package/CLAUDE.md +115 -17
- package/INTEGRATION_SUMMARY.md +21 -20
- package/README.md +82 -48
- package/dist/config/adopt-better-auth-tables.d.ts +107 -0
- package/dist/config/adopt-better-auth-tables.d.ts.map +1 -0
- package/dist/config/adopt-better-auth-tables.js +70 -0
- package/dist/config/adopt-better-auth-tables.js.map +1 -0
- package/dist/config/derive-auth-lists.d.ts +50 -0
- package/dist/config/derive-auth-lists.d.ts.map +1 -0
- package/dist/config/derive-auth-lists.js +274 -0
- package/dist/config/derive-auth-lists.js.map +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +43 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/plugin.d.ts +1 -1
- package/dist/config/plugin.d.ts.map +1 -1
- package/dist/config/plugin.js +52 -9
- package/dist/config/plugin.js.map +1 -1
- package/dist/config/types.d.ts +130 -3
- package/dist/config/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/lists/index.d.ts +17 -11
- package/dist/lists/index.d.ts.map +1 -1
- package/dist/lists/index.js +34 -208
- package/dist/lists/index.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +28 -7
- package/dist/server/index.js.map +1 -1
- package/dist/server/schema-converter.d.ts +1 -1
- package/dist/server/schema-converter.js +1 -1
- package/package.json +3 -3
- package/src/config/adopt-better-auth-tables.ts +146 -0
- package/src/config/derive-auth-lists.ts +323 -0
- package/src/config/index.ts +58 -0
- package/src/config/plugin.ts +67 -10
- package/src/config/types.ts +146 -3
- package/src/index.ts +13 -0
- package/src/lists/index.ts +42 -202
- package/src/server/index.ts +33 -10
- package/src/server/schema-converter.ts +1 -1
- package/tests/adopt-better-auth-tables.test.ts +183 -0
- package/tests/derive-auth-lists.test.ts +232 -0
- package/tests/plugin-derived-keys.test.ts +138 -0
- package/tests/plugin-schema-placement.test.ts +121 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/config/index.ts
CHANGED
|
@@ -1,11 +1,63 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AuthConfig,
|
|
3
|
+
AuthModelConfig,
|
|
3
4
|
NormalizedAuthConfig,
|
|
5
|
+
NormalizedAuthModelConfig,
|
|
6
|
+
NormalizedAuthModels,
|
|
4
7
|
EmailPasswordConfig,
|
|
5
8
|
EmailVerificationConfig,
|
|
6
9
|
PasswordResetConfig,
|
|
7
10
|
} from './types.js'
|
|
8
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Default better-auth model names. Used when the developer does not override
|
|
14
|
+
* `modelName`, preserving the historical `User`/`Session`/`Account`/`Verification`
|
|
15
|
+
* keys exactly.
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_MODEL_NAMES = {
|
|
18
|
+
user: 'User',
|
|
19
|
+
session: 'Session',
|
|
20
|
+
account: 'Account',
|
|
21
|
+
verification: 'Verification',
|
|
22
|
+
} as const
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a single better-auth model config block into its normalized form,
|
|
26
|
+
* falling back to the better-auth default model name and an empty column map.
|
|
27
|
+
* The model's Postgres schema is the per-model `schema` override when present,
|
|
28
|
+
* otherwise the plugin-level `schema` default (or `undefined` for `public`).
|
|
29
|
+
*/
|
|
30
|
+
function normalizeModelConfig(
|
|
31
|
+
config: AuthModelConfig | undefined,
|
|
32
|
+
defaultModelName: string,
|
|
33
|
+
defaultSchema: string | undefined,
|
|
34
|
+
): NormalizedAuthModelConfig {
|
|
35
|
+
return {
|
|
36
|
+
modelName: config?.modelName || defaultModelName,
|
|
37
|
+
fields: config?.fields || {},
|
|
38
|
+
schema: config?.schema ?? defaultSchema,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the better-auth model config for all four auth models.
|
|
44
|
+
* `defaultSchema` is the plugin-level `schema` applied to every model unless a
|
|
45
|
+
* per-model `schema` override is given.
|
|
46
|
+
*/
|
|
47
|
+
function normalizeAuthModels(config: AuthConfig): NormalizedAuthModels {
|
|
48
|
+
const defaultSchema = config.schema
|
|
49
|
+
return {
|
|
50
|
+
user: normalizeModelConfig(config.user, DEFAULT_MODEL_NAMES.user, defaultSchema),
|
|
51
|
+
session: normalizeModelConfig(config.session, DEFAULT_MODEL_NAMES.session, defaultSchema),
|
|
52
|
+
account: normalizeModelConfig(config.account, DEFAULT_MODEL_NAMES.account, defaultSchema),
|
|
53
|
+
verification: normalizeModelConfig(
|
|
54
|
+
config.verification,
|
|
55
|
+
DEFAULT_MODEL_NAMES.verification,
|
|
56
|
+
defaultSchema,
|
|
57
|
+
),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
9
61
|
/**
|
|
10
62
|
* Normalize auth configuration with defaults
|
|
11
63
|
*/
|
|
@@ -47,12 +99,18 @@ export function normalizeAuthConfig(config: AuthConfig): NormalizedAuthConfig {
|
|
|
47
99
|
// Session fields defaults
|
|
48
100
|
const sessionFields = config.sessionFields || ['userId', 'email', 'name']
|
|
49
101
|
|
|
102
|
+
// Resolve better-auth per-model config (modelName + field column maps).
|
|
103
|
+
// Defaults preserve the historical User/Session/Account/Verification keys.
|
|
104
|
+
const models = normalizeAuthModels(config)
|
|
105
|
+
|
|
50
106
|
return {
|
|
51
107
|
emailAndPassword,
|
|
52
108
|
emailVerification,
|
|
53
109
|
passwordReset,
|
|
54
110
|
socialProviders: config.socialProviders || {},
|
|
55
111
|
session,
|
|
112
|
+
models,
|
|
113
|
+
schema: config.schema,
|
|
56
114
|
sessionFields,
|
|
57
115
|
extendUserList: config.extendUserList || {},
|
|
58
116
|
sendEmail:
|
package/src/config/plugin.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Plugin } from '@opensaas/stack-core'
|
|
1
|
+
import type { Plugin } from '@opensaas/stack-core/extend'
|
|
2
|
+
import { getDbKey } from '@opensaas/stack-core'
|
|
2
3
|
import type { AuthConfig, NormalizedAuthConfig } from './types.js'
|
|
3
4
|
import { normalizeAuthConfig } from './index.js'
|
|
4
5
|
import { getAuthLists } from '../lists/index.js'
|
|
@@ -38,8 +39,12 @@ export function authPlugin(config: AuthConfig): Plugin {
|
|
|
38
39
|
},
|
|
39
40
|
|
|
40
41
|
init: async (context) => {
|
|
41
|
-
//
|
|
42
|
-
|
|
42
|
+
// Derive the auth lists from the better-auth model config (modelName +
|
|
43
|
+
// field column maps). With no overrides this yields the historical
|
|
44
|
+
// User/Session/Account/Verification keys; with overrides (e.g.
|
|
45
|
+
// user.modelName: 'AuthUser') the lists are keyed and column-mapped to
|
|
46
|
+
// match the developer's live better-auth tables.
|
|
47
|
+
const authLists = getAuthLists(normalized.extendUserList, normalized.models)
|
|
43
48
|
|
|
44
49
|
// Extract additional lists from Better Auth plugins
|
|
45
50
|
for (const plugin of normalized.betterAuthPlugins) {
|
|
@@ -66,10 +71,18 @@ export function authPlugin(config: AuthConfig): Plugin {
|
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
// Add all auth lists
|
|
74
|
+
// Add all auth lists.
|
|
75
|
+
//
|
|
76
|
+
// The plugin only ever touches its OWN derived keys. When a developer
|
|
77
|
+
// renames the auth user model (e.g. user.modelName: 'AuthUser'), the
|
|
78
|
+
// derived key is 'AuthUser' and an app's separate 'User' list is left
|
|
79
|
+
// untouched — the plugin never extends/overwrites a list it didn't
|
|
80
|
+
// derive. Extending only kicks in when an existing list shares the
|
|
81
|
+
// derived key (e.g. the default 'User'), which is the intended
|
|
82
|
+
// "merge auth fields into my User" behaviour.
|
|
70
83
|
for (const [listName, listConfig] of Object.entries(authLists)) {
|
|
71
84
|
if (context.config.lists[listName]) {
|
|
72
|
-
//
|
|
85
|
+
// A list already exists under this derived key — merge auth fields in.
|
|
73
86
|
context.extendList(listName, {
|
|
74
87
|
fields: listConfig.fields,
|
|
75
88
|
hooks: listConfig.hooks,
|
|
@@ -87,7 +100,54 @@ export function authPlugin(config: AuthConfig): Plugin {
|
|
|
87
100
|
context.setPluginData<NormalizedAuthConfig>('auth', normalized)
|
|
88
101
|
},
|
|
89
102
|
|
|
103
|
+
beforeGenerate: (generationConfig) => {
|
|
104
|
+
// Collect every schema the Auth lists are placed in (per-model schema,
|
|
105
|
+
// else the plugin-level schema). When none is configured the Auth lists
|
|
106
|
+
// stay in the default `public` schema and we leave the config untouched —
|
|
107
|
+
// the greenfield default Prisma schema is unchanged (no `schemas`, no
|
|
108
|
+
// `previewFeatures`, no `@@schema`).
|
|
109
|
+
const authSchemas = Array.from(
|
|
110
|
+
new Set(
|
|
111
|
+
Object.values(normalized.models)
|
|
112
|
+
.map((model) => model.schema)
|
|
113
|
+
.filter((schema): schema is string => Boolean(schema)),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if (authSchemas.length === 0) {
|
|
118
|
+
return generationConfig
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Multi-schema Prisma requires the datasource to list every schema in use
|
|
122
|
+
// AND every model to carry an `@@schema`. Merge the auth schema(s) into the
|
|
123
|
+
// datasource `schemas` array (always including `public` for the app's own
|
|
124
|
+
// lists), and default any list without an explicit `db.schema` to `public`
|
|
125
|
+
// so the generated multi-schema schema is coherent and valid.
|
|
126
|
+
const schemas = Array.from(
|
|
127
|
+
new Set(['public', ...(generationConfig.db.schemas ?? []), ...authSchemas]),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const lists = Object.fromEntries(
|
|
131
|
+
Object.entries(generationConfig.lists).map(([listKey, listConfig]) => {
|
|
132
|
+
if (listConfig.db?.schema) {
|
|
133
|
+
return [listKey, listConfig]
|
|
134
|
+
}
|
|
135
|
+
return [listKey, { ...listConfig, db: { ...listConfig.db, schema: 'public' } }]
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
...generationConfig,
|
|
141
|
+
db: { ...generationConfig.db, schemas },
|
|
142
|
+
lists,
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
90
146
|
runtime: (context) => {
|
|
147
|
+
// Resolve the user list's context.db key from the configured user model.
|
|
148
|
+
// context.db is keyed camelCase, so 'User' -> 'user', 'AuthUser' -> 'authUser'.
|
|
149
|
+
const userDbKey = getDbKey(normalized.models.user.modelName)
|
|
150
|
+
|
|
91
151
|
// Provide auth-related utilities at runtime
|
|
92
152
|
return {
|
|
93
153
|
/**
|
|
@@ -95,9 +155,7 @@ export function authPlugin(config: AuthConfig): Plugin {
|
|
|
95
155
|
* Uses the access-controlled context to fetch user data
|
|
96
156
|
*/
|
|
97
157
|
getUser: async (userId: string) => {
|
|
98
|
-
|
|
99
|
-
const userListKey = 'user' // TODO: Make this configurable based on list name
|
|
100
|
-
return await context.db[userListKey].findUnique({
|
|
158
|
+
return await context.db[userDbKey].findUnique({
|
|
101
159
|
where: { id: userId },
|
|
102
160
|
})
|
|
103
161
|
},
|
|
@@ -110,8 +168,7 @@ export function authPlugin(config: AuthConfig): Plugin {
|
|
|
110
168
|
if (!context.session?.userId) {
|
|
111
169
|
return null
|
|
112
170
|
}
|
|
113
|
-
|
|
114
|
-
return await context.db[userListKey].findUnique({
|
|
171
|
+
return await context.db[userDbKey].findUnique({
|
|
115
172
|
where: { id: context.session.userId },
|
|
116
173
|
})
|
|
117
174
|
},
|
package/src/config/types.ts
CHANGED
|
@@ -82,6 +82,55 @@ export type SessionConfig = {
|
|
|
82
82
|
updateAge?: boolean
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Per-model better-auth configuration block.
|
|
87
|
+
*
|
|
88
|
+
* Mirrors better-auth's own `BetterAuthDBOptions` (the `user`/`session`/
|
|
89
|
+
* `account`/`verification` config a developer already writes): `modelName`
|
|
90
|
+
* renames the table/list and `fields` maps individual better-auth field names
|
|
91
|
+
* to database column names. The auth plugin derives its Auth lists from this
|
|
92
|
+
* config so the generated lists carry the same keys and column maps as the
|
|
93
|
+
* developer's live better-auth tables.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* authPlugin({
|
|
98
|
+
* user: { modelName: 'AuthUser', fields: { name: 'full_name' } },
|
|
99
|
+
* session: { modelName: 'AuthSession' },
|
|
100
|
+
* })
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export type AuthModelConfig = {
|
|
104
|
+
/**
|
|
105
|
+
* The table/list name for this model.
|
|
106
|
+
* Becomes the OpenSaaS list key (and Prisma model name) and the table `@@map`.
|
|
107
|
+
* @default the default better-auth model name (e.g. 'User', 'Session')
|
|
108
|
+
*/
|
|
109
|
+
modelName?: string
|
|
110
|
+
/**
|
|
111
|
+
* Map better-auth field names to database column names.
|
|
112
|
+
* Each entry generates a `@map("column")` on the derived field.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* fields: { name: 'full_name', emailVerified: 'email_verified' }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
fields?: Record<string, string>
|
|
120
|
+
/**
|
|
121
|
+
* Database schema (Postgres) for this auth model.
|
|
122
|
+
* Generates a `@@schema("...")` on the derived list, overriding the
|
|
123
|
+
* plugin-level {@link AuthConfig.schema} for this one model.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* // Place the verification table in a different schema from the rest
|
|
128
|
+
* verification: { schema: 'auth_internal' }
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
schema?: string
|
|
132
|
+
}
|
|
133
|
+
|
|
85
134
|
/**
|
|
86
135
|
* Auth configuration options
|
|
87
136
|
*/
|
|
@@ -107,9 +156,60 @@ export type AuthConfig = {
|
|
|
107
156
|
socialProviders?: SocialProvidersConfig
|
|
108
157
|
|
|
109
158
|
/**
|
|
110
|
-
* Session configuration
|
|
159
|
+
* Session configuration.
|
|
160
|
+
*
|
|
161
|
+
* Carries session expiry settings as well as the better-auth `session` model
|
|
162
|
+
* config (`modelName` + field column `fields` maps) used to derive the Auth
|
|
163
|
+
* session list.
|
|
164
|
+
*/
|
|
165
|
+
session?: SessionConfig & AuthModelConfig
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* better-auth `user` model configuration (modelName + field column maps).
|
|
169
|
+
* Used to derive the Auth user list's key, table `@@map`, and field `@map`s.
|
|
170
|
+
*
|
|
171
|
+
* Custom fields beyond the better-auth basics are added via `extendUserList`.
|
|
172
|
+
*/
|
|
173
|
+
user?: AuthModelConfig
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* better-auth `account` model configuration (modelName + field column maps).
|
|
177
|
+
*/
|
|
178
|
+
account?: AuthModelConfig
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* better-auth `verification` model configuration (modelName + field column maps).
|
|
182
|
+
*/
|
|
183
|
+
verification?: AuthModelConfig
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Database schema (Postgres) for the generated Auth lists.
|
|
187
|
+
*
|
|
188
|
+
* When set, all four Auth lists (user/session/account/verification) are placed
|
|
189
|
+
* in this schema via `@@schema(...)`, and the stack's multi-schema support is
|
|
190
|
+
* wired automatically: the datasource `schemas` array gains this schema (plus
|
|
191
|
+
* `public`) and the `multiSchema` preview feature is enabled. A per-model
|
|
192
|
+
* {@link AuthModelConfig.schema} overrides this for an individual list.
|
|
193
|
+
*
|
|
194
|
+
* Useful for adopting an existing separate-schema better-auth installation
|
|
195
|
+
* (e.g. an `auth` Postgres schema) so the generated lists diff clean against
|
|
196
|
+
* the live tables. When unset, the Auth lists stay in the default `public`
|
|
197
|
+
* schema and no `@@schema` is emitted (greenfield default unchanged).
|
|
198
|
+
*
|
|
199
|
+
* Only applies to the `postgresql` provider.
|
|
200
|
+
*
|
|
201
|
+
* @example Adopt an `auth`-schema better-auth install
|
|
202
|
+
* ```typescript
|
|
203
|
+
* authPlugin({
|
|
204
|
+
* schema: 'auth',
|
|
205
|
+
* user: { modelName: 'AuthUser' },
|
|
206
|
+
* session: { modelName: 'AuthSession' },
|
|
207
|
+
* account: { modelName: 'AuthAccount' },
|
|
208
|
+
* verification: { modelName: 'AuthVerification' },
|
|
209
|
+
* })
|
|
210
|
+
* ```
|
|
111
211
|
*/
|
|
112
|
-
|
|
212
|
+
schema?: string
|
|
113
213
|
|
|
114
214
|
/**
|
|
115
215
|
* Which fields to include in the session object
|
|
@@ -202,6 +302,30 @@ export type AuthConfig = {
|
|
|
202
302
|
}
|
|
203
303
|
}
|
|
204
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Resolved per-model auth configuration after normalization.
|
|
307
|
+
* Always carries a concrete `modelName` (the developer's override or the
|
|
308
|
+
* better-auth default) and a (possibly empty) `fields` column map. `schema`
|
|
309
|
+
* carries the resolved Postgres schema for the model (per-model override, else
|
|
310
|
+
* the plugin-level schema, else `undefined` for the default `public` schema).
|
|
311
|
+
*/
|
|
312
|
+
export type NormalizedAuthModelConfig = {
|
|
313
|
+
modelName: string
|
|
314
|
+
fields: Record<string, string>
|
|
315
|
+
schema?: string
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Resolved auth model configuration for all four better-auth models.
|
|
320
|
+
* Consumed by the Auth-list derivation and the runtime user-key resolution.
|
|
321
|
+
*/
|
|
322
|
+
export type NormalizedAuthModels = {
|
|
323
|
+
user: NormalizedAuthModelConfig
|
|
324
|
+
session: NormalizedAuthModelConfig
|
|
325
|
+
account: NormalizedAuthModelConfig
|
|
326
|
+
verification: NormalizedAuthModelConfig
|
|
327
|
+
}
|
|
328
|
+
|
|
205
329
|
/**
|
|
206
330
|
* Internal normalized auth configuration
|
|
207
331
|
* Used after parsing user config
|
|
@@ -209,12 +333,31 @@ export type AuthConfig = {
|
|
|
209
333
|
export type NormalizedAuthConfig = Required<
|
|
210
334
|
Omit<
|
|
211
335
|
AuthConfig,
|
|
212
|
-
|
|
336
|
+
| 'emailAndPassword'
|
|
337
|
+
| 'emailVerification'
|
|
338
|
+
| 'passwordReset'
|
|
339
|
+
| 'betterAuthPlugins'
|
|
340
|
+
| 'rateLimit'
|
|
341
|
+
| 'session'
|
|
342
|
+
| 'user'
|
|
343
|
+
| 'account'
|
|
344
|
+
| 'verification'
|
|
345
|
+
| 'schema'
|
|
213
346
|
>
|
|
214
347
|
> & {
|
|
215
348
|
emailAndPassword: Required<EmailPasswordConfig>
|
|
216
349
|
emailVerification: Required<EmailVerificationConfig>
|
|
217
350
|
passwordReset: Required<PasswordResetConfig>
|
|
351
|
+
/** Resolved session expiry settings (model config lives under `models.session`). */
|
|
352
|
+
session: Required<SessionConfig>
|
|
353
|
+
/** Resolved better-auth model config (modelName + field column maps + schema) for all auth models. */
|
|
354
|
+
models: NormalizedAuthModels
|
|
355
|
+
/**
|
|
356
|
+
* Plugin-level Postgres schema for the Auth lists, if any. Resolved per-model
|
|
357
|
+
* schemas live on `models.<model>.schema`; this is the unresolved plugin-level
|
|
358
|
+
* default (used to wire the datasource `schemas` array during generation).
|
|
359
|
+
*/
|
|
360
|
+
schema?: string
|
|
218
361
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Better Auth plugin types are not exposed, must use any
|
|
219
362
|
betterAuthPlugins: any[]
|
|
220
363
|
rateLimit?: {
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,19 @@ export { authPlugin } from './config/plugin.js'
|
|
|
34
34
|
export type { AuthConfig, NormalizedAuthConfig } from './config/index.js'
|
|
35
35
|
export type * from './config/types.js'
|
|
36
36
|
|
|
37
|
+
// Pure better-auth config -> Auth lists derivation (advanced use cases)
|
|
38
|
+
export { deriveAuthLists } from './config/derive-auth-lists.js'
|
|
39
|
+
export type { DerivedAuthLists } from './config/derive-auth-lists.js'
|
|
40
|
+
|
|
41
|
+
// "Adopt existing better-auth tables" recipe — sets the model/schema knobs that
|
|
42
|
+
// match a pre-existing separate-schema better-auth install so a migrating
|
|
43
|
+
// project reaches Schema parity without rebuilding the config by hand.
|
|
44
|
+
export { adoptBetterAuthTables } from './config/adopt-better-auth-tables.js'
|
|
45
|
+
export type {
|
|
46
|
+
AdoptBetterAuthTablesOptions,
|
|
47
|
+
AdoptBetterAuthTablesConfig,
|
|
48
|
+
} from './config/adopt-better-auth-tables.js'
|
|
49
|
+
|
|
37
50
|
// Runtime type exports
|
|
38
51
|
export type { AuthRuntimeServices } from './runtime/types.js'
|
|
39
52
|
|
package/src/lists/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { list } from '@opensaas/stack-core'
|
|
2
|
-
import { text, timestamp, checkbox, relationship } from '@opensaas/stack-core/fields'
|
|
3
1
|
import type { ListConfig, FieldConfig } from '@opensaas/stack-core'
|
|
2
|
+
import type { NormalizedAuthModels } from '../config/types.js'
|
|
3
|
+
import { deriveAuthLists } from '../config/derive-auth-lists.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Configuration for extending the auto-generated User list
|
|
@@ -25,229 +25,69 @@ export type ExtendUserListConfig = {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* The default better-auth model config (no `modelName`/`fields` overrides).
|
|
29
|
+
* Produces the historical `User`/`Session`/`Account`/`Verification` keys with
|
|
30
|
+
* their original field shapes. Used by the backwards-compatible
|
|
31
|
+
* `createUserList`/`getAuthLists` helpers.
|
|
32
|
+
*/
|
|
33
|
+
const DEFAULT_MODELS: NormalizedAuthModels = {
|
|
34
|
+
user: { modelName: 'User', fields: {} },
|
|
35
|
+
session: { modelName: 'Session', fields: {} },
|
|
36
|
+
account: { modelName: 'Account', fields: {} },
|
|
37
|
+
verification: { modelName: 'Verification', fields: {} },
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create the base User list with better-auth required fields.
|
|
42
|
+
*
|
|
43
|
+
* Backwards-compatible helper: derives the default `User` list (keyed `User`,
|
|
44
|
+
* default field shapes) via {@link deriveAuthLists}.
|
|
30
45
|
*/
|
|
31
46
|
export function createUserList(
|
|
32
|
-
config?: ExtendUserListConfig,
|
|
47
|
+
config?: ExtendUserListConfig,
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
33
49
|
): ListConfig<any> {
|
|
34
|
-
return
|
|
35
|
-
fields: {
|
|
36
|
-
// Better-auth required fields
|
|
37
|
-
name: text({
|
|
38
|
-
validation: { isRequired: true },
|
|
39
|
-
}),
|
|
40
|
-
email: text({
|
|
41
|
-
validation: { isRequired: true },
|
|
42
|
-
isIndexed: 'unique',
|
|
43
|
-
}),
|
|
44
|
-
emailVerified: checkbox({
|
|
45
|
-
defaultValue: false,
|
|
46
|
-
}),
|
|
47
|
-
image: text(),
|
|
48
|
-
|
|
49
|
-
// Relationships to other auth tables
|
|
50
|
-
sessions: relationship({
|
|
51
|
-
ref: 'Session.user',
|
|
52
|
-
many: true,
|
|
53
|
-
}),
|
|
54
|
-
accounts: relationship({
|
|
55
|
-
ref: 'Account.user',
|
|
56
|
-
many: true,
|
|
57
|
-
}),
|
|
58
|
-
|
|
59
|
-
// Custom fields from user config
|
|
60
|
-
...(config?.fields || {}),
|
|
61
|
-
},
|
|
62
|
-
access: config?.access || {
|
|
63
|
-
operation: {
|
|
64
|
-
// Anyone can query users (for displaying names, etc.)
|
|
65
|
-
query: () => true,
|
|
66
|
-
// Anyone can create a user (sign up)
|
|
67
|
-
create: () => true,
|
|
68
|
-
// Only update your own user record
|
|
69
|
-
update: ({ session, item }) => {
|
|
70
|
-
if (!session) return false
|
|
71
|
-
const userId = (session as { userId?: string }).userId
|
|
72
|
-
const itemId = (item as { id?: string })?.id
|
|
73
|
-
return userId === itemId
|
|
74
|
-
},
|
|
75
|
-
// Only delete your own user record
|
|
76
|
-
delete: ({ session, item }) => {
|
|
77
|
-
if (!session) return false
|
|
78
|
-
const userId = (session as { userId?: string }).userId
|
|
79
|
-
const itemId = (item as { id?: string })?.id
|
|
80
|
-
return userId === itemId
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
hooks: config?.hooks,
|
|
85
|
-
})
|
|
50
|
+
return deriveAuthLists(DEFAULT_MODELS, config).lists.User
|
|
86
51
|
}
|
|
87
52
|
|
|
88
53
|
/**
|
|
89
|
-
* Create the Session list for better-auth
|
|
90
|
-
* Stores active user sessions
|
|
54
|
+
* Create the Session list for better-auth (default `Session` key).
|
|
91
55
|
*/
|
|
92
56
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
93
57
|
export function createSessionList(): ListConfig<any> {
|
|
94
|
-
return
|
|
95
|
-
fields: {
|
|
96
|
-
// Session token (stored in cookie, used as primary key)
|
|
97
|
-
token: text({
|
|
98
|
-
validation: { isRequired: true },
|
|
99
|
-
isIndexed: 'unique',
|
|
100
|
-
}),
|
|
101
|
-
// Expiration timestamp
|
|
102
|
-
expiresAt: timestamp(),
|
|
103
|
-
// Optional: IP address for security
|
|
104
|
-
ipAddress: text(),
|
|
105
|
-
// Optional: User agent for security
|
|
106
|
-
userAgent: text(),
|
|
107
|
-
// Relationship to user (userId will be auto-generated)
|
|
108
|
-
user: relationship({
|
|
109
|
-
ref: 'User.sessions',
|
|
110
|
-
}),
|
|
111
|
-
},
|
|
112
|
-
access: {
|
|
113
|
-
operation: {
|
|
114
|
-
// Only the session owner can query their sessions
|
|
115
|
-
query: ({ session }) => {
|
|
116
|
-
if (!session) return false
|
|
117
|
-
const userId = (session as { userId?: string }).userId
|
|
118
|
-
if (!userId) return false
|
|
119
|
-
// Return Prisma filter for nested relationship
|
|
120
|
-
return {
|
|
121
|
-
user: {
|
|
122
|
-
id: { equals: userId },
|
|
123
|
-
},
|
|
124
|
-
} as Record<string, unknown>
|
|
125
|
-
},
|
|
126
|
-
// Better-auth handles session creation
|
|
127
|
-
create: () => true,
|
|
128
|
-
// No manual updates
|
|
129
|
-
update: () => false,
|
|
130
|
-
// Better-auth handles session deletion (logout)
|
|
131
|
-
delete: ({ session, item }) => {
|
|
132
|
-
if (!session) return false
|
|
133
|
-
const userId = (session as { userId?: string }).userId
|
|
134
|
-
const itemUserId = (item as { user?: { id?: string } })?.user?.id
|
|
135
|
-
return userId === itemUserId
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
})
|
|
58
|
+
return deriveAuthLists(DEFAULT_MODELS).lists.Session
|
|
140
59
|
}
|
|
141
60
|
|
|
142
61
|
/**
|
|
143
|
-
* Create the Account list for better-auth
|
|
144
|
-
* Stores OAuth provider accounts and credentials
|
|
62
|
+
* Create the Account list for better-auth (default `Account` key).
|
|
145
63
|
*/
|
|
146
64
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
147
65
|
export function createAccountList(): ListConfig<any> {
|
|
148
|
-
return
|
|
149
|
-
fields: {
|
|
150
|
-
// Account identifier from provider
|
|
151
|
-
accountId: text({
|
|
152
|
-
validation: { isRequired: true },
|
|
153
|
-
}),
|
|
154
|
-
// Provider identifier (e.g., 'github', 'google', 'credentials')
|
|
155
|
-
providerId: text({
|
|
156
|
-
validation: { isRequired: true },
|
|
157
|
-
}),
|
|
158
|
-
// Relationship to user (userId will be auto-generated)
|
|
159
|
-
user: relationship({
|
|
160
|
-
ref: 'User.accounts',
|
|
161
|
-
}),
|
|
162
|
-
// OAuth tokens
|
|
163
|
-
accessToken: text(),
|
|
164
|
-
refreshToken: text(),
|
|
165
|
-
accessTokenExpiresAt: timestamp(),
|
|
166
|
-
refreshTokenExpiresAt: timestamp(),
|
|
167
|
-
scope: text(),
|
|
168
|
-
idToken: text(),
|
|
169
|
-
// Password hash for credential provider (better-auth stores in account table)
|
|
170
|
-
password: text(),
|
|
171
|
-
},
|
|
172
|
-
access: {
|
|
173
|
-
operation: {
|
|
174
|
-
// Only the account owner can query their accounts
|
|
175
|
-
query: ({ session }) => {
|
|
176
|
-
if (!session) return false
|
|
177
|
-
const userId = (session as { userId?: string }).userId
|
|
178
|
-
if (!userId) return false
|
|
179
|
-
// Return Prisma filter for nested relationship
|
|
180
|
-
return {
|
|
181
|
-
user: {
|
|
182
|
-
id: { equals: userId },
|
|
183
|
-
},
|
|
184
|
-
} as Record<string, unknown>
|
|
185
|
-
},
|
|
186
|
-
// Better-auth handles account creation
|
|
187
|
-
create: () => true,
|
|
188
|
-
// Better-auth handles account updates (token refresh)
|
|
189
|
-
update: ({ session, item }) => {
|
|
190
|
-
if (!session) return false
|
|
191
|
-
const userId = (session as { userId?: string }).userId
|
|
192
|
-
const itemUserId = (item as { user?: { id?: string } })?.user?.id
|
|
193
|
-
return userId === itemUserId
|
|
194
|
-
},
|
|
195
|
-
// Account owner can delete their accounts
|
|
196
|
-
delete: ({ session, item }) => {
|
|
197
|
-
if (!session) return false
|
|
198
|
-
const userId = (session as { userId?: string }).userId
|
|
199
|
-
const itemUserId = (item as { user?: { id?: string } })?.user?.id
|
|
200
|
-
return userId === itemUserId
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
})
|
|
66
|
+
return deriveAuthLists(DEFAULT_MODELS).lists.Account
|
|
205
67
|
}
|
|
206
68
|
|
|
207
69
|
/**
|
|
208
|
-
* Create the Verification list for better-auth
|
|
209
|
-
* Stores email verification tokens, password reset tokens, etc.
|
|
70
|
+
* Create the Verification list for better-auth (default `Verification` key).
|
|
210
71
|
*/
|
|
211
72
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
212
73
|
export function createVerificationList(): ListConfig<any> {
|
|
213
|
-
return
|
|
214
|
-
fields: {
|
|
215
|
-
// Identifier (e.g., email address)
|
|
216
|
-
identifier: text({
|
|
217
|
-
validation: { isRequired: true },
|
|
218
|
-
}),
|
|
219
|
-
// Token value
|
|
220
|
-
value: text({
|
|
221
|
-
validation: { isRequired: true },
|
|
222
|
-
}),
|
|
223
|
-
// Expiration timestamp
|
|
224
|
-
expiresAt: timestamp(),
|
|
225
|
-
},
|
|
226
|
-
access: {
|
|
227
|
-
operation: {
|
|
228
|
-
// No public querying (better-auth handles verification internally)
|
|
229
|
-
query: () => false,
|
|
230
|
-
// Better-auth creates verification tokens
|
|
231
|
-
create: () => true,
|
|
232
|
-
// No updates
|
|
233
|
-
update: () => false,
|
|
234
|
-
// Better-auth deletes used/expired tokens
|
|
235
|
-
delete: () => true,
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
})
|
|
74
|
+
return deriveAuthLists(DEFAULT_MODELS).lists.Verification
|
|
239
75
|
}
|
|
240
76
|
|
|
241
77
|
/**
|
|
242
|
-
* Get all auth lists required by better-auth
|
|
243
|
-
*
|
|
78
|
+
* Get all auth lists required by better-auth.
|
|
79
|
+
*
|
|
80
|
+
* Derives the Auth lists from the resolved better-auth model config. When no
|
|
81
|
+
* `models` are supplied (or none carry overrides), the result is the historical
|
|
82
|
+
* default set keyed `User`/`Session`/`Account`/`Verification`.
|
|
83
|
+
*
|
|
84
|
+
* @param userConfig - Extra User-list fields/access/hooks (from `extendUserList`)
|
|
85
|
+
* @param models - Resolved better-auth model config; defaults to the better-auth defaults
|
|
244
86
|
*/
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
Verification: createVerificationList(),
|
|
252
|
-
}
|
|
87
|
+
export function getAuthLists(
|
|
88
|
+
userConfig?: ExtendUserListConfig,
|
|
89
|
+
models: NormalizedAuthModels = DEFAULT_MODELS,
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
91
|
+
): Record<string, ListConfig<any>> {
|
|
92
|
+
return deriveAuthLists(models, userConfig || {}).lists
|
|
253
93
|
}
|