@opensaas/stack-auth 0.1.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +217 -0
  3. package/CLAUDE.md +61 -0
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +1 -0
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/config/plugin.d.ts.map +1 -1
  8. package/dist/config/plugin.js +33 -1
  9. package/dist/config/plugin.js.map +1 -1
  10. package/dist/config/types.d.ts +38 -1
  11. package/dist/config/types.d.ts.map +1 -1
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/lists/index.d.ts +7 -7
  16. package/dist/lists/index.d.ts.map +1 -1
  17. package/dist/lists/index.js +4 -0
  18. package/dist/lists/index.js.map +1 -1
  19. package/dist/runtime/types.d.ts +26 -0
  20. package/dist/runtime/types.d.ts.map +1 -0
  21. package/dist/runtime/types.js +6 -0
  22. package/dist/runtime/types.js.map +1 -0
  23. package/dist/server/index.d.ts.map +1 -1
  24. package/dist/server/index.js +42 -9
  25. package/dist/server/index.js.map +1 -1
  26. package/dist/server/schema-converter.d.ts +2 -2
  27. package/dist/server/schema-converter.d.ts.map +1 -1
  28. package/dist/server/schema-converter.js +12 -1
  29. package/dist/server/schema-converter.js.map +1 -1
  30. package/dist/ui/components/ForgotPasswordForm.js +1 -1
  31. package/dist/ui/components/ForgotPasswordForm.js.map +1 -1
  32. package/dist/ui/components/SignInForm.d.ts.map +1 -1
  33. package/dist/ui/components/SignInForm.js +11 -1
  34. package/dist/ui/components/SignInForm.js.map +1 -1
  35. package/dist/ui/components/SignUpForm.d.ts.map +1 -1
  36. package/dist/ui/components/SignUpForm.js +11 -1
  37. package/dist/ui/components/SignUpForm.js.map +1 -1
  38. package/package.json +12 -13
  39. package/src/config/index.ts +1 -0
  40. package/src/config/plugin.ts +37 -2
  41. package/src/config/types.ts +42 -1
  42. package/src/index.ts +3 -0
  43. package/src/lists/index.ts +15 -7
  44. package/src/runtime/types.ts +27 -0
  45. package/src/server/index.ts +47 -9
  46. package/src/server/schema-converter.ts +27 -14
  47. package/src/ui/components/ForgotPasswordForm.tsx +1 -1
  48. package/src/ui/components/SignInForm.tsx +10 -1
  49. package/src/ui/components/SignUpForm.tsx +10 -1
  50. package/tests/config.test.ts +4 -13
  51. package/tsconfig.tsbuildinfo +1 -1
@@ -15,18 +15,22 @@ export type ExtendUserListConfig = {
15
15
  * Access control for the User list
16
16
  * If not provided, defaults to basic access control (users can update their own records)
17
17
  */
18
- access?: ListConfig['access']
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
19
+ access?: ListConfig<any>['access']
19
20
  /**
20
21
  * Hooks for the User list
21
22
  */
22
- hooks?: ListConfig['hooks']
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
24
+ hooks?: ListConfig<any>['hooks']
23
25
  }
24
26
 
25
27
  /**
26
28
  * Create the base User list with better-auth required fields
27
29
  * This matches the better-auth user schema
28
30
  */
29
- export function createUserList(config?: ExtendUserListConfig): ListConfig {
31
+ export function createUserList(
32
+ config?: ExtendUserListConfig, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
33
+ ): ListConfig<any> {
30
34
  return list({
31
35
  fields: {
32
36
  // Better-auth required fields
@@ -85,7 +89,8 @@ export function createUserList(config?: ExtendUserListConfig): ListConfig {
85
89
  * Create the Session list for better-auth
86
90
  * Stores active user sessions
87
91
  */
88
- export function createSessionList(): ListConfig {
92
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
93
+ export function createSessionList(): ListConfig<any> {
89
94
  return list({
90
95
  fields: {
91
96
  // Session token (stored in cookie, used as primary key)
@@ -138,7 +143,8 @@ export function createSessionList(): ListConfig {
138
143
  * Create the Account list for better-auth
139
144
  * Stores OAuth provider accounts and credentials
140
145
  */
141
- export function createAccountList(): ListConfig {
146
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
147
+ export function createAccountList(): ListConfig<any> {
142
148
  return list({
143
149
  fields: {
144
150
  // Account identifier from provider
@@ -202,7 +208,8 @@ export function createAccountList(): ListConfig {
202
208
  * Create the Verification list for better-auth
203
209
  * Stores email verification tokens, password reset tokens, etc.
204
210
  */
205
- export function createVerificationList(): ListConfig {
211
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
212
+ export function createVerificationList(): ListConfig<any> {
206
213
  return list({
207
214
  fields: {
208
215
  // Identifier (e.g., email address)
@@ -235,7 +242,8 @@ export function createVerificationList(): ListConfig {
235
242
  * Get all auth lists required by better-auth
236
243
  * This is the main export used by withAuth()
237
244
  */
238
- export function getAuthLists(userConfig?: ExtendUserListConfig): Record<string, ListConfig> {
245
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
246
+ export function getAuthLists(userConfig?: ExtendUserListConfig): Record<string, ListConfig<any>> {
239
247
  return {
240
248
  User: createUserList(userConfig),
241
249
  Session: createSessionList(),
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Type definitions for auth plugin runtime services
3
+ * These types are used for type-safe access to context.plugins.auth
4
+ */
5
+
6
+ /**
7
+ * Runtime services provided by the auth plugin
8
+ * Available via context.plugins.auth
9
+ */
10
+ export interface AuthRuntimeServices {
11
+ /**
12
+ * Get user by ID
13
+ * Uses the access-controlled context to fetch user data
14
+ *
15
+ * @param userId - The ID of the user to fetch
16
+ * @returns User object or null if not found or access denied
17
+ */
18
+ getUser: (userId: string) => Promise<unknown>
19
+
20
+ /**
21
+ * Get current user from session
22
+ * Extracts userId from session and fetches user data
23
+ *
24
+ * @returns Current user object or null if not authenticated or not found
25
+ */
26
+ getCurrentUser: () => Promise<unknown>
27
+ }
@@ -98,6 +98,15 @@ export function createAuth(
98
98
  {} as Record<string, { clientId: string; clientSecret: string }>,
99
99
  ),
100
100
 
101
+ // Rate limiting configuration
102
+ rateLimit: authConfig.rateLimit
103
+ ? {
104
+ enabled: authConfig.rateLimit.enabled,
105
+ window: authConfig.rateLimit.window,
106
+ max: authConfig.rateLimit.max,
107
+ }
108
+ : undefined,
109
+
101
110
  // Pass through any additional Better Auth plugins
102
111
  plugins: authConfig.betterAuthPlugins || [],
103
112
  }
@@ -113,16 +122,45 @@ export function createAuth(
113
122
  // Return a proxy that lazily initializes the auth instance
114
123
  return new Proxy({} as ReturnType<typeof betterAuth>, {
115
124
  get(_, prop) {
116
- return (...args: unknown[]) => {
117
- return (async () => {
118
- const instance = await getAuthInstance()
119
- const value = instance[prop as keyof typeof instance]
120
- if (typeof value === 'function') {
121
- return (value as (...args: unknown[]) => unknown).apply(instance, args)
122
- }
123
- return value
124
- })()
125
+ if (prop === 'then') {
126
+ // Support await on the proxy itself
127
+ return undefined
128
+ }
129
+
130
+ // Create a lazy wrapper function
131
+ const lazyWrapper = async (...args: unknown[]) => {
132
+ const instance = await getAuthInstance()
133
+ const value = instance[prop as keyof typeof instance]
134
+ if (typeof value === 'function') {
135
+ return (value as (...args: unknown[]) => unknown).apply(instance, args)
136
+ }
137
+ return value
125
138
  }
139
+
140
+ // Return a proxy that supports both direct calls and nested property access
141
+ return new Proxy(lazyWrapper, {
142
+ get(target, subProp) {
143
+ if (subProp === 'then') {
144
+ // Support await on nested properties
145
+ return undefined
146
+ }
147
+ // Handle nested property access (e.g., auth.api.getSession)
148
+ return async (...args: unknown[]) => {
149
+ const instance = await getAuthInstance()
150
+ const parentValue = instance[prop as keyof typeof instance]
151
+ if (parentValue && typeof parentValue === 'object') {
152
+ const childValue = (parentValue as Record<string, unknown>)[subProp as string]
153
+ if (typeof childValue === 'function') {
154
+ return (childValue as (...args: unknown[]) => unknown).apply(parentValue, args)
155
+ }
156
+ return childValue
157
+ }
158
+ throw new Error(
159
+ `Property ${String(prop)}.${String(subProp)} not found on auth instance`,
160
+ )
161
+ }
162
+ },
163
+ })
126
164
  },
127
165
  })
128
166
  }
@@ -94,7 +94,8 @@ function convertField(
94
94
  * Get default access control for auth tables
95
95
  * Most auth tables should only be accessible to their owners
96
96
  */
97
- function getDefaultAccess(tableName: string): ListConfig['access'] {
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
98
+ function getDefaultAccess(tableName: string): ListConfig<any>['access'] {
98
99
  const lowerTableName = tableName.toLowerCase()
99
100
 
100
101
  // User table - special access control
@@ -103,13 +104,15 @@ function getDefaultAccess(tableName: string): ListConfig['access'] {
103
104
  operation: {
104
105
  query: () => true, // Anyone can query users
105
106
  create: () => true, // Anyone can create (sign up)
106
- update: ({ session, item }) => {
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
108
+ update: ({ session, item }: any) => {
107
109
  if (!session) return false
108
110
  const userId = (session as { userId?: string }).userId
109
111
  const itemId = (item as { id?: string })?.id
110
112
  return userId === itemId
111
113
  },
112
- delete: ({ session, item }) => {
114
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
115
+ delete: ({ session, item }: any) => {
113
116
  if (!session) return false
114
117
  const userId = (session as { userId?: string }).userId
115
118
  const itemId = (item as { id?: string })?.id
@@ -123,7 +126,8 @@ function getDefaultAccess(tableName: string): ListConfig['access'] {
123
126
  if (lowerTableName === 'session') {
124
127
  return {
125
128
  operation: {
126
- query: ({ session }) => {
129
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
130
+ query: ({ session }: any) => {
127
131
  if (!session) return false
128
132
  const userId = (session as { userId?: string }).userId
129
133
  if (!userId) return false
@@ -133,7 +137,8 @@ function getDefaultAccess(tableName: string): ListConfig['access'] {
133
137
  },
134
138
  create: () => true, // Better-auth handles session creation
135
139
  update: () => false, // No manual updates
136
- delete: ({ session, item }) => {
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
141
+ delete: ({ session, item }: any) => {
137
142
  if (!session) return false
138
143
  const userId = (session as { userId?: string }).userId
139
144
  const itemUserId = (item as { user?: { id?: string } })?.user?.id
@@ -147,7 +152,8 @@ function getDefaultAccess(tableName: string): ListConfig['access'] {
147
152
  if (lowerTableName === 'account') {
148
153
  return {
149
154
  operation: {
150
- query: ({ session }) => {
155
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
156
+ query: ({ session }: any) => {
151
157
  if (!session) return false
152
158
  const userId = (session as { userId?: string }).userId
153
159
  if (!userId) return false
@@ -156,13 +162,15 @@ function getDefaultAccess(tableName: string): ListConfig['access'] {
156
162
  } as Record<string, unknown>
157
163
  },
158
164
  create: () => true, // Better-auth handles account creation
159
- update: ({ session, item }) => {
165
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
166
+ update: ({ session, item }: any) => {
160
167
  if (!session) return false
161
168
  const userId = (session as { userId?: string }).userId
162
169
  const itemUserId = (item as { user?: { id?: string } })?.user?.id
163
170
  return userId === itemUserId
164
171
  },
165
- delete: ({ session, item }) => {
172
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
173
+ delete: ({ session, item }: any) => {
166
174
  if (!session) return false
167
175
  const userId = (session as { userId?: string }).userId
168
176
  const itemUserId = (item as { user?: { id?: string } })?.user?.id
@@ -192,7 +200,8 @@ function getDefaultAccess(tableName: string): ListConfig['access'] {
192
200
  ) {
193
201
  return {
194
202
  operation: {
195
- query: ({ session }) => {
203
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
204
+ query: ({ session }: any) => {
196
205
  if (!session) return false
197
206
  const userId = (session as { userId?: string }).userId
198
207
  if (!userId) return false
@@ -202,13 +211,15 @@ function getDefaultAccess(tableName: string): ListConfig['access'] {
202
211
  } as Record<string, unknown>
203
212
  },
204
213
  create: () => true, // Better-auth/plugins handle creation
205
- update: ({ session, item }) => {
214
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
215
+ update: ({ session, item }: any) => {
206
216
  if (!session) return false
207
217
  const userId = (session as { userId?: string }).userId
208
218
  const itemUserId = (item as { userId?: string })?.userId
209
219
  return userId === itemUserId
210
220
  },
211
- delete: ({ session, item }) => {
221
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Access control parameters are runtime values
222
+ delete: ({ session, item }: any) => {
212
223
  if (!session) return false
213
224
  const userId = (session as { userId?: string }).userId
214
225
  const itemUserId = (item as { userId?: string })?.userId
@@ -235,7 +246,8 @@ function getDefaultAccess(tableName: string): ListConfig['access'] {
235
246
  export function convertTableToList(
236
247
  tableName: string,
237
248
  tableSchema: BetterAuthTableSchema,
238
- ): ListConfig {
249
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
250
+ ): ListConfig<any> {
239
251
  const fields: Record<string, FieldConfig> = {}
240
252
 
241
253
  // First pass: convert regular fields
@@ -275,8 +287,9 @@ export function convertTableToList(
275
287
  */
276
288
  export function convertBetterAuthSchema(
277
289
  tables: Record<string, BetterAuthTableSchema>,
278
- ): Record<string, ListConfig> {
279
- const lists: Record<string, ListConfig> = {}
290
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
291
+ ): Record<string, ListConfig<any>> {
292
+ const lists: Record<string, ListConfig<any>> = {} // eslint-disable-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
280
293
 
281
294
  for (const [tableName, tableSchema] of Object.entries(tables)) {
282
295
  // Convert table name to PascalCase for OpenSaaS list key
@@ -55,7 +55,7 @@ export function ForgotPasswordForm({
55
55
  setLoading(true)
56
56
 
57
57
  try {
58
- const result = await authClient.forgetPassword({
58
+ const result = await authClient.requestPasswordReset({
59
59
  email,
60
60
  redirectTo: '/reset-password',
61
61
  })
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import React, { useState } from 'react'
4
+ import { useRouter } from 'next/navigation.js'
4
5
  import type { createAuthClient } from 'better-auth/react'
5
6
 
6
7
  export type SignInFormProps = {
@@ -62,6 +63,7 @@ export function SignInForm({
62
63
  onSuccess,
63
64
  onError,
64
65
  }: SignInFormProps) {
66
+ const router = useRouter()
65
67
  const [email, setEmail] = useState('')
66
68
  const [password, setPassword] = useState('')
67
69
  const [error, setError] = useState('')
@@ -83,7 +85,12 @@ export function SignInForm({
83
85
  throw new Error(result.error.message)
84
86
  }
85
87
 
86
- onSuccess?.()
88
+ // If onSuccess is provided, call it. Otherwise, automatically redirect
89
+ if (onSuccess) {
90
+ onSuccess()
91
+ } else {
92
+ router.push(redirectTo)
93
+ }
87
94
  } catch (err) {
88
95
  const message = err instanceof Error ? err.message : 'Sign in failed'
89
96
  setError(message)
@@ -102,6 +109,8 @@ export function SignInForm({
102
109
  provider,
103
110
  callbackURL: redirectTo,
104
111
  })
112
+ // Social sign-in handles its own redirect via OAuth flow
113
+ // Only call onSuccess if provided
105
114
  onSuccess?.()
106
115
  } catch (err) {
107
116
  const message = err instanceof Error ? err.message : 'Sign in failed'
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import React, { useState } from 'react'
4
+ import { useRouter } from 'next/navigation.js'
4
5
  import type { createAuthClient } from 'better-auth/react'
5
6
 
6
7
  export type SignUpFormProps = {
@@ -67,6 +68,7 @@ export function SignUpForm({
67
68
  onSuccess,
68
69
  onError,
69
70
  }: SignUpFormProps) {
71
+ const router = useRouter()
70
72
  const [name, setName] = useState('')
71
73
  const [email, setEmail] = useState('')
72
74
  const [password, setPassword] = useState('')
@@ -98,7 +100,12 @@ export function SignUpForm({
98
100
  throw new Error(result.error.message)
99
101
  }
100
102
 
101
- onSuccess?.()
103
+ // If onSuccess is provided, call it. Otherwise, automatically redirect
104
+ if (onSuccess) {
105
+ onSuccess()
106
+ } else {
107
+ router.push(redirectTo)
108
+ }
102
109
  } catch (err) {
103
110
  const message = err instanceof Error ? err.message : 'Sign up failed'
104
111
  setError(message)
@@ -117,6 +124,8 @@ export function SignUpForm({
117
124
  provider,
118
125
  callbackURL: redirectTo,
119
126
  })
127
+ // Social sign-in handles its own redirect via OAuth flow
128
+ // Only call onSuccess if provided
120
129
  onSuccess?.()
121
130
  } catch (err) {
122
131
  const message = err instanceof Error ? err.message : 'Sign up failed'
@@ -154,7 +154,6 @@ describe('authPlugin', () => {
154
154
  const result = await config({
155
155
  db: {
156
156
  provider: 'sqlite',
157
- url: 'file:./test.db',
158
157
  },
159
158
  plugins: [authPlugin({})],
160
159
  lists: {
@@ -175,22 +174,23 @@ describe('authPlugin', () => {
175
174
  })
176
175
 
177
176
  it('should preserve database config', async () => {
177
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
+ const mockConstructor = (() => null) as any
178
179
  const result = await config({
179
180
  db: {
180
181
  provider: 'postgresql',
181
- url: 'postgresql://test',
182
+ prismaClientConstructor: mockConstructor,
182
183
  },
183
184
  plugins: [authPlugin({})],
184
185
  lists: {},
185
186
  })
186
187
 
187
188
  expect(result.db.provider).toBe('postgresql')
188
- expect(result.db.url).toBe('postgresql://test')
189
+ expect(result.db.prismaClientConstructor).toBe(mockConstructor)
189
190
  })
190
191
 
191
192
  it('should store normalized auth config in _pluginData', async () => {
192
193
  const result = await config({
193
- db: { provider: 'sqlite', url: 'file:./test.db' },
194
194
  plugins: [
195
195
  authPlugin({
196
196
  emailAndPassword: { enabled: true, minPasswordLength: 12 },
@@ -209,7 +209,6 @@ describe('authPlugin', () => {
209
209
 
210
210
  it('should extend User list with custom fields', async () => {
211
211
  const result = await config({
212
- db: { provider: 'sqlite', url: 'file:./test.db' },
213
212
  plugins: [
214
213
  authPlugin({
215
214
  extendUserList: {
@@ -239,7 +238,6 @@ describe('authPlugin', () => {
239
238
 
240
239
  it('should generate User list with correct fields', async () => {
241
240
  const result = await config({
242
- db: { provider: 'sqlite', url: 'file:./test.db' },
243
241
  plugins: [authPlugin({})],
244
242
  lists: {},
245
243
  })
@@ -256,7 +254,6 @@ describe('authPlugin', () => {
256
254
 
257
255
  it('should generate Session list with correct fields', async () => {
258
256
  const result = await config({
259
- db: { provider: 'sqlite', url: 'file:./test.db' },
260
257
  plugins: [authPlugin({})],
261
258
  lists: {},
262
259
  })
@@ -272,7 +269,6 @@ describe('authPlugin', () => {
272
269
 
273
270
  it('should generate Account list with correct fields', async () => {
274
271
  const result = await config({
275
- db: { provider: 'sqlite', url: 'file:./test.db' },
276
272
  plugins: [authPlugin({})],
277
273
  lists: {},
278
274
  })
@@ -289,7 +285,6 @@ describe('authPlugin', () => {
289
285
 
290
286
  it('should generate Verification list with correct fields', async () => {
291
287
  const result = await config({
292
- db: { provider: 'sqlite', url: 'file:./test.db' },
293
288
  plugins: [authPlugin({})],
294
289
  lists: {},
295
290
  })
@@ -303,7 +298,6 @@ describe('authPlugin', () => {
303
298
 
304
299
  it('should work with empty auth config', async () => {
305
300
  const result = await config({
306
- db: { provider: 'sqlite', url: 'file:./test.db' },
307
301
  plugins: [authPlugin({})],
308
302
  lists: {},
309
303
  })
@@ -316,7 +310,6 @@ describe('authPlugin', () => {
316
310
 
317
311
  it('should merge with other user-defined lists', async () => {
318
312
  const result = await config({
319
- db: { provider: 'sqlite', url: 'file:./test.db' },
320
313
  plugins: [authPlugin({})],
321
314
  lists: {
322
315
  Post: list({
@@ -344,7 +337,6 @@ describe('authPlugin', () => {
344
337
 
345
338
  it('should pass through config options to normalized config', async () => {
346
339
  const result = await config({
347
- db: { provider: 'sqlite', url: 'file:./test.db' },
348
340
  plugins: [
349
341
  authPlugin({
350
342
  emailAndPassword: {
@@ -407,7 +399,6 @@ describe('authPlugin', () => {
407
399
  }
408
400
 
409
401
  const result = await config({
410
- db: { provider: 'sqlite', url: 'file:./test.db' },
411
402
  plugins: [
412
403
  authPlugin({
413
404
  betterAuthPlugins: [mockPlugin],