@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
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getAuthTables } from 'better-auth/db'
|
|
3
|
+
import { mcp } from '@better-auth/mcp'
|
|
4
|
+
import type { BetterAuthOptions } from 'better-auth'
|
|
5
|
+
import type { DBFieldAttribute } from 'better-auth/db'
|
|
6
|
+
import type { FieldConfig } from '@opensaas/stack-core'
|
|
7
|
+
import type { RelationshipField } from '@opensaas/stack-core/fields'
|
|
8
|
+
import { deriveAuthLists } from '../src/config/derive-auth-lists.js'
|
|
9
|
+
import type { NormalizedAuthModels } from '../src/config/types.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Drift detector for issue #988: reads better-auth's own resolved table
|
|
13
|
+
* definitions (`getAuthTables`) and compares them against `deriveAuthLists`'
|
|
14
|
+
* output — column name, index presence, uniqueness, field presence,
|
|
15
|
+
* requiredness, references, scalar type, and static default values — so an
|
|
16
|
+
* upstream schema change that the hand-written derivation doesn't mirror
|
|
17
|
+
* fails this suite instead of waiting for a human to notice (as
|
|
18
|
+
* #921/#935/#937/#981/#986 all were).
|
|
19
|
+
*
|
|
20
|
+
* Deliberately out of scope (see the issue body): fields the stack adds that
|
|
21
|
+
* better-auth doesn't declare (timestamps, access blocks, extendUserList
|
|
22
|
+
* merging), constraint *names* (#921), and `returned: false` (a serialization
|
|
23
|
+
* contract, not a schema fact).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
type Divergence = {
|
|
27
|
+
model: string
|
|
28
|
+
field: string
|
|
29
|
+
dimension: string
|
|
30
|
+
detail: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* better-auth models its foreign keys as a plain scalar field carrying
|
|
35
|
+
* `references`; the derived lists model the same column as a `relationship()`
|
|
36
|
+
* field under a different key. This is the map between the two per model.
|
|
37
|
+
*/
|
|
38
|
+
const FK_BRIDGE: Record<string, Record<string, string>> = {
|
|
39
|
+
session: { userId: 'user' },
|
|
40
|
+
account: { userId: 'user' },
|
|
41
|
+
// The MCP plugin's OAuth tables (issue #992) — `clientId`/`resourceId` are
|
|
42
|
+
// deliberately NOT bridged here: they reference oauthClient.clientId /
|
|
43
|
+
// oauthResource.identifier, not an `id`, so they stay plain scalar columns
|
|
44
|
+
// and are compared via `compareScalarField`.
|
|
45
|
+
oauthClient: { userId: 'user' },
|
|
46
|
+
oauthRefreshToken: { userId: 'user', sessionId: 'session' },
|
|
47
|
+
oauthAccessToken: { userId: 'user', sessionId: 'session', refreshId: 'refresh' },
|
|
48
|
+
oauthConsent: { userId: 'user' },
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** better-auth writes these on every row; the derived lists opt in via list-level `db.timestamps` rather than a per-field entry. */
|
|
52
|
+
const TIMESTAMP_FIELDS = new Set(['createdAt', 'updatedAt'])
|
|
53
|
+
|
|
54
|
+
function normalizeOnDelete(action: string): string {
|
|
55
|
+
return action.replace(/\s+/g, '').toLowerCase()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The Prisma scalar type a correctly-derived field should carry for a given
|
|
60
|
+
* better-auth field type. `undefined` means "not modeled by any field in
|
|
61
|
+
* these lists" (json/array types don't appear on user/session/account/
|
|
62
|
+
* verification/rateLimit) — the caller skips the comparison in that case
|
|
63
|
+
* rather than treating it as a mismatch.
|
|
64
|
+
*/
|
|
65
|
+
function expectedPrismaType(upstream: DBFieldAttribute): string | undefined {
|
|
66
|
+
switch (upstream.type) {
|
|
67
|
+
case 'string':
|
|
68
|
+
return 'String'
|
|
69
|
+
case 'boolean':
|
|
70
|
+
return 'Boolean'
|
|
71
|
+
case 'date':
|
|
72
|
+
return 'DateTime'
|
|
73
|
+
case 'number':
|
|
74
|
+
return upstream.bigint ? 'BigInt' : 'Int'
|
|
75
|
+
default:
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Narrows a derived list's field to a `RelationshipField` — the shape the FK-bridged comparison reads. */
|
|
81
|
+
function isRelationshipField(field: FieldConfig): field is RelationshipField {
|
|
82
|
+
return field.type === 'relationship'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function compareScalarField(
|
|
86
|
+
model: string,
|
|
87
|
+
fieldKey: string,
|
|
88
|
+
upstream: DBFieldAttribute,
|
|
89
|
+
derived: FieldConfig | undefined,
|
|
90
|
+
divergences: Divergence[],
|
|
91
|
+
): void {
|
|
92
|
+
if (!derived || !derived.getPrismaType) {
|
|
93
|
+
divergences.push({
|
|
94
|
+
model,
|
|
95
|
+
field: fieldKey,
|
|
96
|
+
dimension: 'field presence',
|
|
97
|
+
detail: `better-auth declares '${fieldKey}' but the derived list has no matching field`,
|
|
98
|
+
})
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const prismaType = derived.getPrismaType(fieldKey)
|
|
103
|
+
const isNullable = prismaType.modifiers?.startsWith('?') ?? false
|
|
104
|
+
const isUnique = prismaType.modifiers?.includes('@unique') ?? false
|
|
105
|
+
const isIndexed = prismaType.index === true
|
|
106
|
+
|
|
107
|
+
// Base models always declare an explicit `fieldName`; a plugin table
|
|
108
|
+
// doesn't have to (`getAuthTables` merges plugin schema fields verbatim,
|
|
109
|
+
// with no per-field defaulting) — an unset `fieldName` means the column IS
|
|
110
|
+
// the field key, same as `scalarFieldDb` in `derive-auth-lists.ts` assumes.
|
|
111
|
+
const upstreamColumn = upstream.fieldName ?? fieldKey
|
|
112
|
+
const derivedColumn = derived.db?.map ?? fieldKey
|
|
113
|
+
if (derivedColumn !== upstreamColumn) {
|
|
114
|
+
divergences.push({
|
|
115
|
+
model,
|
|
116
|
+
field: fieldKey,
|
|
117
|
+
dimension: 'column name',
|
|
118
|
+
detail: `better-auth column "${upstreamColumn}" vs derived map "${derivedColumn}"`,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const upstreamRequired = upstream.required ?? true
|
|
123
|
+
if (isNullable === upstreamRequired) {
|
|
124
|
+
divergences.push({
|
|
125
|
+
model,
|
|
126
|
+
field: fieldKey,
|
|
127
|
+
dimension: 'requiredness',
|
|
128
|
+
detail: `better-auth required=${upstreamRequired} vs derived nullable=${isNullable}`,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const upstreamIndex = upstream.index === true
|
|
133
|
+
if (isIndexed !== upstreamIndex) {
|
|
134
|
+
divergences.push({
|
|
135
|
+
model,
|
|
136
|
+
field: fieldKey,
|
|
137
|
+
dimension: 'index presence',
|
|
138
|
+
detail: `better-auth index=${upstreamIndex} vs derived isIndexed=true is ${isIndexed}`,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const upstreamUnique = upstream.unique === true
|
|
143
|
+
if (isUnique !== upstreamUnique) {
|
|
144
|
+
divergences.push({
|
|
145
|
+
model,
|
|
146
|
+
field: fieldKey,
|
|
147
|
+
dimension: 'uniqueness',
|
|
148
|
+
detail: `better-auth unique=${upstreamUnique} vs derived isIndexed='unique' is ${isUnique}`,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const expectedType = expectedPrismaType(upstream)
|
|
153
|
+
if (expectedType !== undefined && prismaType.type !== expectedType) {
|
|
154
|
+
divergences.push({
|
|
155
|
+
model,
|
|
156
|
+
field: fieldKey,
|
|
157
|
+
dimension: 'scalar type',
|
|
158
|
+
detail: `better-auth type "${upstream.type}"${upstream.bigint ? ' (bigint)' : ''} expects Prisma "${expectedType}" vs derived "${prismaType.type}"`,
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Only a static, non-function default on a modeled type is comparable —
|
|
163
|
+
// better-auth's function-valued defaults (e.g. `createdAt: () => new
|
|
164
|
+
// Date()`) are applied at write time, not modeled as a derived field's own
|
|
165
|
+
// `defaultValue`; a type this derivation doesn't model at all (`json`,
|
|
166
|
+
// `string[]`, ...) falls back to `text()` (see `expectedType` above), which
|
|
167
|
+
// can't faithfully carry that type's own default either.
|
|
168
|
+
if (
|
|
169
|
+
expectedType !== undefined &&
|
|
170
|
+
typeof upstream.defaultValue !== 'function' &&
|
|
171
|
+
upstream.defaultValue !== undefined
|
|
172
|
+
) {
|
|
173
|
+
if (derived.defaultValue !== upstream.defaultValue) {
|
|
174
|
+
divergences.push({
|
|
175
|
+
model,
|
|
176
|
+
field: fieldKey,
|
|
177
|
+
dimension: 'default value',
|
|
178
|
+
detail: `better-auth defaultValue=${JSON.stringify(upstream.defaultValue)} vs derived defaultValue=${JSON.stringify(derived.defaultValue)}`,
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function compareForeignKeyField(
|
|
185
|
+
model: string,
|
|
186
|
+
upstreamFieldKey: string,
|
|
187
|
+
upstream: DBFieldAttribute,
|
|
188
|
+
derived: RelationshipField | undefined,
|
|
189
|
+
listKeys: Record<string, string>,
|
|
190
|
+
divergences: Divergence[],
|
|
191
|
+
): void {
|
|
192
|
+
if (!derived) {
|
|
193
|
+
divergences.push({
|
|
194
|
+
model,
|
|
195
|
+
field: upstreamFieldKey,
|
|
196
|
+
dimension: 'field presence',
|
|
197
|
+
detail: `better-auth declares '${upstreamFieldKey}' but the derived relationship is missing`,
|
|
198
|
+
})
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// See the same `fieldName ?? fieldKey` note in `compareScalarField` above.
|
|
203
|
+
const upstreamColumn = upstream.fieldName ?? upstreamFieldKey
|
|
204
|
+
const foreignKey = derived.db?.foreignKey
|
|
205
|
+
const derivedColumn = typeof foreignKey === 'object' ? foreignKey.map : undefined
|
|
206
|
+
if (derivedColumn !== upstreamColumn) {
|
|
207
|
+
divergences.push({
|
|
208
|
+
model,
|
|
209
|
+
field: upstreamFieldKey,
|
|
210
|
+
dimension: 'column name',
|
|
211
|
+
detail: `better-auth column "${upstreamColumn}" vs derived FK map "${derivedColumn}"`,
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const isIndexed = derived.isIndexed === true
|
|
216
|
+
const upstreamIndex = upstream.index === true
|
|
217
|
+
if (isIndexed !== upstreamIndex) {
|
|
218
|
+
divergences.push({
|
|
219
|
+
model,
|
|
220
|
+
field: upstreamFieldKey,
|
|
221
|
+
dimension: 'index presence',
|
|
222
|
+
detail: `better-auth index=${upstreamIndex} vs derived isIndexed=true is ${isIndexed}`,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const isUnique = derived.isIndexed === 'unique'
|
|
227
|
+
const upstreamUnique = upstream.unique === true
|
|
228
|
+
if (isUnique !== upstreamUnique) {
|
|
229
|
+
divergences.push({
|
|
230
|
+
model,
|
|
231
|
+
field: upstreamFieldKey,
|
|
232
|
+
dimension: 'uniqueness',
|
|
233
|
+
detail: `better-auth unique=${upstreamUnique} vs derived isIndexed='unique' is ${isUnique}`,
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const isNullable = derived.db?.isNullable ?? true
|
|
238
|
+
const derivedRequired = !isNullable
|
|
239
|
+
const upstreamRequired = upstream.required ?? true
|
|
240
|
+
if (derivedRequired !== upstreamRequired) {
|
|
241
|
+
divergences.push({
|
|
242
|
+
model,
|
|
243
|
+
field: upstreamFieldKey,
|
|
244
|
+
dimension: 'requiredness',
|
|
245
|
+
detail: `better-auth required=${upstreamRequired} vs derived required=${derivedRequired}`,
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const references = upstream.references
|
|
250
|
+
if (references) {
|
|
251
|
+
const [targetListKey] = derived.ref.split('.')
|
|
252
|
+
const expectedListKey = listKeys[references.model]
|
|
253
|
+
if (targetListKey !== expectedListKey) {
|
|
254
|
+
divergences.push({
|
|
255
|
+
model,
|
|
256
|
+
field: upstreamFieldKey,
|
|
257
|
+
dimension: 'references',
|
|
258
|
+
detail: `better-auth references model "${references.model}" (list "${expectedListKey}") vs derived ref targets "${targetListKey}"`,
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const extend = derived.db?.extendPrismaSchema
|
|
263
|
+
const relationLine = extend
|
|
264
|
+
? extend({ relationLine: '@relation(fields: [x], references: [id])' }).relationLine
|
|
265
|
+
: ''
|
|
266
|
+
const derivedOnDelete = /onDelete:\s*(\w+)/.exec(relationLine)?.[1] ?? ''
|
|
267
|
+
const upstreamOnDelete = references.onDelete ?? 'cascade'
|
|
268
|
+
if (normalizeOnDelete(derivedOnDelete) !== normalizeOnDelete(upstreamOnDelete)) {
|
|
269
|
+
divergences.push({
|
|
270
|
+
model,
|
|
271
|
+
field: upstreamFieldKey,
|
|
272
|
+
dimension: 'references',
|
|
273
|
+
detail: `better-auth onDelete="${upstreamOnDelete}" vs derived relation line "${relationLine}"`,
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function compareModel(
|
|
280
|
+
model: string,
|
|
281
|
+
upstreamFields: Record<string, DBFieldAttribute>,
|
|
282
|
+
derivedFields: Record<string, FieldConfig>,
|
|
283
|
+
listKeys: Record<string, string>,
|
|
284
|
+
divergences: Divergence[],
|
|
285
|
+
): void {
|
|
286
|
+
const bridges = FK_BRIDGE[model] ?? {}
|
|
287
|
+
|
|
288
|
+
for (const [fieldKey, upstream] of Object.entries(upstreamFields)) {
|
|
289
|
+
if (TIMESTAMP_FIELDS.has(fieldKey)) continue
|
|
290
|
+
|
|
291
|
+
const bridgedKey = bridges[fieldKey]
|
|
292
|
+
if (bridgedKey) {
|
|
293
|
+
const candidate = derivedFields[bridgedKey]
|
|
294
|
+
const relationshipField = candidate && isRelationshipField(candidate) ? candidate : undefined
|
|
295
|
+
compareForeignKeyField(model, fieldKey, upstream, relationshipField, listKeys, divergences)
|
|
296
|
+
} else {
|
|
297
|
+
compareScalarField(model, fieldKey, upstream, derivedFields[fieldKey], divergences)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Known, tracked divergences the comparison above would otherwise fail on.
|
|
304
|
+
* Each entry must name the field, dimension, and issue tracking the fix.
|
|
305
|
+
* When the fix lands, remove the entry — an entry that no longer matches a
|
|
306
|
+
* real divergence fails `no stale exceptions` below, so this list can't rot
|
|
307
|
+
* into a permanent silencer.
|
|
308
|
+
*/
|
|
309
|
+
const KNOWN_EXCEPTIONS: Divergence[] = []
|
|
310
|
+
|
|
311
|
+
function exceptionKey(d: Divergence): string {
|
|
312
|
+
return `${d.model}.${d.field}:${d.dimension}`
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
describe('derived auth lists match better-auth’s own table definitions (issue #988)', () => {
|
|
316
|
+
const defaultModels: NormalizedAuthModels = {
|
|
317
|
+
user: { modelName: 'User', fields: {} },
|
|
318
|
+
session: { modelName: 'Session', fields: {} },
|
|
319
|
+
account: { modelName: 'Account', fields: {} },
|
|
320
|
+
verification: { modelName: 'Verification', fields: {} },
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const defaultOptions: BetterAuthOptions = {}
|
|
324
|
+
const upstreamTables = getAuthTables(defaultOptions)
|
|
325
|
+
const { keys, lists } = deriveAuthLists(defaultModels)
|
|
326
|
+
|
|
327
|
+
const divergences: Divergence[] = []
|
|
328
|
+
for (const model of ['user', 'session', 'account', 'verification'] as const) {
|
|
329
|
+
const listKey = keys[model]
|
|
330
|
+
compareModel(model, upstreamTables[model].fields, lists[listKey].fields, keys, divergences)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const unexpected = divergences.filter(
|
|
334
|
+
(d) => !KNOWN_EXCEPTIONS.some((known) => exceptionKey(known) === exceptionKey(d)),
|
|
335
|
+
)
|
|
336
|
+
const staleExceptions = KNOWN_EXCEPTIONS.filter(
|
|
337
|
+
(known) => !divergences.some((d) => exceptionKey(d) === exceptionKey(known)),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
it('has no unexpected divergences from better-auth’s user/session/account/verification tables', () => {
|
|
341
|
+
expect(unexpected, JSON.stringify(unexpected, null, 2)).toEqual([])
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('has no stale exception-list entries (fixed drift must be removed from KNOWN_EXCEPTIONS)', () => {
|
|
345
|
+
expect(staleExceptions, JSON.stringify(staleExceptions, null, 2)).toEqual([])
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('detects a real regression: removing isIndexed from Session.user', () => {
|
|
349
|
+
const mutated = deriveAuthLists(defaultModels)
|
|
350
|
+
mutated.lists[mutated.keys.session].fields.user.isIndexed = false
|
|
351
|
+
|
|
352
|
+
const regressionDivergences: Divergence[] = []
|
|
353
|
+
compareModel(
|
|
354
|
+
'session',
|
|
355
|
+
upstreamTables.session.fields,
|
|
356
|
+
mutated.lists[mutated.keys.session].fields,
|
|
357
|
+
mutated.keys,
|
|
358
|
+
regressionDivergences,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
expect(regressionDivergences).toContainEqual(
|
|
362
|
+
expect.objectContaining({ model: 'session', field: 'userId', dimension: 'index presence' }),
|
|
363
|
+
)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('detects a real regression: changing the userId FK column map', () => {
|
|
367
|
+
const mutated = deriveAuthLists(defaultModels)
|
|
368
|
+
mutated.lists[mutated.keys.account].fields.user.db.foreignKey = { map: 'wrong_column' }
|
|
369
|
+
|
|
370
|
+
const regressionDivergences: Divergence[] = []
|
|
371
|
+
compareModel(
|
|
372
|
+
'account',
|
|
373
|
+
upstreamTables.account.fields,
|
|
374
|
+
mutated.lists[mutated.keys.account].fields,
|
|
375
|
+
mutated.keys,
|
|
376
|
+
regressionDivergences,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
expect(regressionDivergences).toContainEqual(
|
|
380
|
+
expect.objectContaining({ model: 'account', field: 'userId', dimension: 'column name' }),
|
|
381
|
+
)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('detects a real regression: a field upstream declares going missing from the derived list', () => {
|
|
385
|
+
const mutated = deriveAuthLists(defaultModels)
|
|
386
|
+
delete mutated.lists[mutated.keys.account].fields.scope
|
|
387
|
+
|
|
388
|
+
const regressionDivergences: Divergence[] = []
|
|
389
|
+
compareModel(
|
|
390
|
+
'account',
|
|
391
|
+
upstreamTables.account.fields,
|
|
392
|
+
mutated.lists[mutated.keys.account].fields,
|
|
393
|
+
mutated.keys,
|
|
394
|
+
regressionDivergences,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
expect(regressionDivergences).toContainEqual(
|
|
398
|
+
expect.objectContaining({ model: 'account', field: 'scope', dimension: 'field presence' }),
|
|
399
|
+
)
|
|
400
|
+
})
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
describe('derived RateLimit list matches better-auth’s database-backed rateLimit table (issue #988)', () => {
|
|
404
|
+
const models: NormalizedAuthModels = {
|
|
405
|
+
user: { modelName: 'User', fields: {} },
|
|
406
|
+
session: { modelName: 'Session', fields: {} },
|
|
407
|
+
account: { modelName: 'Account', fields: {} },
|
|
408
|
+
verification: { modelName: 'Verification', fields: {} },
|
|
409
|
+
rateLimit: { modelName: 'RateLimit', fields: {} },
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const rateLimitOptions: BetterAuthOptions = { rateLimit: { storage: 'database' } }
|
|
413
|
+
const upstreamTables = getAuthTables(rateLimitOptions)
|
|
414
|
+
const { keys, lists } = deriveAuthLists(models)
|
|
415
|
+
|
|
416
|
+
it('is present upstream only when rateLimit.storage is "database"', () => {
|
|
417
|
+
expect(upstreamTables.rateLimit).toBeDefined()
|
|
418
|
+
expect(keys.rateLimit).toBe('RateLimit')
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('has no divergences on key/count/lastRequest', () => {
|
|
422
|
+
const divergences: Divergence[] = []
|
|
423
|
+
compareModel(
|
|
424
|
+
'rateLimit',
|
|
425
|
+
upstreamTables.rateLimit.fields,
|
|
426
|
+
lists.RateLimit.fields,
|
|
427
|
+
keys,
|
|
428
|
+
divergences,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
expect(divergences, JSON.stringify(divergences, null, 2)).toEqual([])
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
describe('derived better-auth plugin tables match better-auth’s own table definitions (issue #992)', () => {
|
|
436
|
+
const defaultModels: NormalizedAuthModels = {
|
|
437
|
+
user: { modelName: 'User', fields: {} },
|
|
438
|
+
session: { modelName: 'Session', fields: {} },
|
|
439
|
+
account: { modelName: 'Account', fields: {} },
|
|
440
|
+
verification: { modelName: 'Verification', fields: {} },
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const plugin = mcp({
|
|
444
|
+
loginPage: '/sign-in',
|
|
445
|
+
consentPage: '/consent',
|
|
446
|
+
resource: 'https://example.com/mcp',
|
|
447
|
+
})
|
|
448
|
+
const upstreamTables = getAuthTables({ plugins: [plugin] })
|
|
449
|
+
const { keys, lists } = deriveAuthLists(defaultModels, {}, {}, [plugin])
|
|
450
|
+
const pluginModelToListKey: Record<string, string> = {
|
|
451
|
+
oauthClient: 'OauthClient',
|
|
452
|
+
oauthClientAssertion: 'OauthClientAssertion',
|
|
453
|
+
oauthClientResource: 'OauthClientResource',
|
|
454
|
+
oauthAccessToken: 'OauthAccessToken',
|
|
455
|
+
oauthRefreshToken: 'OauthRefreshToken',
|
|
456
|
+
oauthResource: 'OauthResource',
|
|
457
|
+
oauthConsent: 'OauthConsent',
|
|
458
|
+
}
|
|
459
|
+
const listKeys = { ...keys, ...pluginModelToListKey }
|
|
460
|
+
|
|
461
|
+
const divergences: Divergence[] = []
|
|
462
|
+
for (const model of Object.keys(pluginModelToListKey)) {
|
|
463
|
+
const listKey = pluginModelToListKey[model]
|
|
464
|
+
compareModel(model, upstreamTables[model].fields, lists[listKey].fields, listKeys, divergences)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const unexpected = divergences.filter(
|
|
468
|
+
(d) => !KNOWN_EXCEPTIONS.some((known) => exceptionKey(known) === exceptionKey(d)),
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
it('has no unexpected divergences from better-auth’s MCP plugin OAuth tables', () => {
|
|
472
|
+
expect(unexpected, JSON.stringify(unexpected, null, 2)).toEqual([])
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it('derives every OAuth table under a PascalCase key', () => {
|
|
476
|
+
for (const listKey of Object.values(pluginModelToListKey)) {
|
|
477
|
+
expect(lists).toHaveProperty(listKey)
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
})
|
package/tests/config.test.ts
CHANGED
|
@@ -252,6 +252,8 @@ describe('normalizeAuthConfig', () => {
|
|
|
252
252
|
modelName: 'RateLimit',
|
|
253
253
|
tableName: undefined,
|
|
254
254
|
fields: {},
|
|
255
|
+
schema: undefined,
|
|
256
|
+
indexes: [],
|
|
255
257
|
})
|
|
256
258
|
})
|
|
257
259
|
|
|
@@ -279,6 +281,7 @@ describe('normalizeAuthConfig', () => {
|
|
|
279
281
|
tableName: 'rate_limit',
|
|
280
282
|
fields: { key: 'limit_key' },
|
|
281
283
|
schema: 'auth',
|
|
284
|
+
indexes: [],
|
|
282
285
|
})
|
|
283
286
|
})
|
|
284
287
|
|