@opensaas/stack-auth 0.1.5 → 0.1.6

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.
@@ -1,4 +1,3 @@
1
- import type { OpenSaasConfig } from '@opensaas/stack-core'
2
1
  import type {
3
2
  AuthConfig,
4
3
  NormalizedAuthConfig,
@@ -6,8 +5,6 @@ import type {
6
5
  EmailVerificationConfig,
7
6
  PasswordResetConfig,
8
7
  } from './types.js'
9
- import { getAuthLists } from '../lists/index.js'
10
- import { convertBetterAuthSchema } from '../server/schema-converter.js'
11
8
 
12
9
  /**
13
10
  * Normalize auth configuration with defaults
@@ -70,97 +67,5 @@ export function normalizeAuthConfig(config: AuthConfig): NormalizedAuthConfig {
70
67
  }
71
68
  }
72
69
 
73
- /**
74
- * Auth configuration builder
75
- * Use this to create an auth configuration object
76
- *
77
- * @example
78
- * ```typescript
79
- * import { authConfig } from '@opensaas/stack-auth'
80
- *
81
- * const auth = authConfig({
82
- * emailAndPassword: { enabled: true },
83
- * emailVerification: { enabled: true },
84
- * socialProviders: {
85
- * github: { clientId: '...', clientSecret: '...' }
86
- * }
87
- * })
88
- * ```
89
- */
90
- export function authConfig(config: AuthConfig): AuthConfig {
91
- return config
92
- }
93
-
94
- /**
95
- * Wrap an OpenSaas config with better-auth integration
96
- * This merges the auth lists into the user's config and sets up session handling
97
- *
98
- * @example
99
- * ```typescript
100
- * import { config } from '@opensaas/stack-core'
101
- * import { withAuth, authConfig } from '@opensaas/stack-auth'
102
- *
103
- * export default withAuth(
104
- * config({
105
- * db: { provider: 'sqlite', url: 'file:./dev.db' },
106
- * lists: {
107
- * Post: list({ ... })
108
- * }
109
- * }),
110
- * authConfig({
111
- * emailAndPassword: { enabled: true }
112
- * })
113
- * )
114
- * ```
115
- */
116
- export function withAuth(opensaasConfig: OpenSaasConfig, authConfig: AuthConfig): OpenSaasConfig {
117
- const normalized = normalizeAuthConfig(authConfig)
118
-
119
- // Get auth lists from plugins
120
- const authLists = getAuthListsFromPlugins(normalized)
121
-
122
- // Merge auth lists with user lists (auth lists take priority)
123
- const mergedLists = {
124
- ...opensaasConfig.lists,
125
- ...authLists,
126
- }
127
-
128
- // Return merged config with auth config attached
129
- // Note: Session integration happens in the generator/context
130
- const result: OpenSaasConfig & { __authConfig?: NormalizedAuthConfig } = {
131
- ...opensaasConfig,
132
- lists: mergedLists,
133
- }
134
-
135
- // Store auth config for internal use
136
- result.__authConfig = normalized
137
-
138
- return result
139
- }
140
-
141
- /**
142
- * Get auth lists by extracting schemas from Better Auth plugins
143
- * This inspects the plugin objects directly without requiring a database connection
144
- */
145
- function getAuthListsFromPlugins(authConfig: NormalizedAuthConfig) {
146
- // Start with base Better Auth tables (always required)
147
- const authLists = getAuthLists(authConfig.extendUserList)
148
-
149
- // Extract additional tables from plugins
150
- for (const plugin of authConfig.betterAuthPlugins) {
151
- if (plugin && typeof plugin === 'object' && 'schema' in plugin) {
152
- // Plugin has schema property - convert to OpenSaaS lists
153
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin schema types are dynamic
154
- const pluginSchema = plugin.schema as any
155
-
156
- // Convert plugin schema to OpenSaaS lists and merge
157
- const pluginLists = convertBetterAuthSchema(pluginSchema)
158
- Object.assign(authLists, pluginLists)
159
- }
160
- }
161
-
162
- return authLists
163
- }
164
-
165
70
  export type { AuthConfig, NormalizedAuthConfig }
166
71
  export * from './types.js'
@@ -0,0 +1,86 @@
1
+ import type { Plugin } from '@opensaas/stack-core'
2
+ import type { AuthConfig, NormalizedAuthConfig } from './types.js'
3
+ import { normalizeAuthConfig } from './index.js'
4
+ import { getAuthLists } from '../lists/index.js'
5
+ import { convertBetterAuthSchema } from '../server/schema-converter.js'
6
+
7
+ /**
8
+ * Auth plugin for OpenSaas Stack
9
+ * Provides Better-auth integration with automatic list generation and session management
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { config } from '@opensaas/stack-core'
14
+ * import { authPlugin } from '@opensaas/stack-auth'
15
+ *
16
+ * export default config({
17
+ * plugins: [
18
+ * authPlugin({
19
+ * emailAndPassword: { enabled: true },
20
+ * sessionFields: ['userId', 'email', 'name', 'role']
21
+ * })
22
+ * ],
23
+ * db: { provider: 'sqlite', url: 'file:./dev.db' },
24
+ * lists: { Post: list({...}) }
25
+ * })
26
+ * ```
27
+ */
28
+ export function authPlugin(config: AuthConfig): Plugin {
29
+ const normalized = normalizeAuthConfig(config)
30
+
31
+ return {
32
+ name: 'auth',
33
+ version: '0.1.0',
34
+
35
+ init: async (context) => {
36
+ // Get auth lists from base Better Auth schema
37
+ const authLists = getAuthLists(normalized.extendUserList)
38
+
39
+ // Extract additional lists from Better Auth plugins
40
+ for (const plugin of normalized.betterAuthPlugins) {
41
+ if (plugin && typeof plugin === 'object' && 'schema' in plugin) {
42
+ // Plugin has schema property - convert to OpenSaaS lists
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin schema types are dynamic
44
+ const pluginSchema = plugin.schema as any
45
+ const pluginLists = convertBetterAuthSchema(pluginSchema)
46
+
47
+ // Add or extend lists from plugin
48
+ for (const [listName, listConfig] of Object.entries(pluginLists)) {
49
+ if (context.config.lists[listName]) {
50
+ // List exists, extend it
51
+ context.extendList(listName, {
52
+ fields: listConfig.fields,
53
+ hooks: listConfig.hooks,
54
+ access: listConfig.access,
55
+ mcp: listConfig.mcp,
56
+ })
57
+ } else {
58
+ // List doesn't exist, add it
59
+ context.addList(listName, listConfig)
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ // Add all auth lists
66
+ for (const [listName, listConfig] of Object.entries(authLists)) {
67
+ if (context.config.lists[listName]) {
68
+ // If user defined a User list, extend it with auth fields
69
+ context.extendList(listName, {
70
+ fields: listConfig.fields,
71
+ hooks: listConfig.hooks,
72
+ access: listConfig.access,
73
+ mcp: listConfig.mcp,
74
+ })
75
+ } else {
76
+ // Otherwise, add the auth list
77
+ context.addList(listName, listConfig)
78
+ }
79
+ }
80
+
81
+ // Store auth config for runtime access
82
+ // Access at runtime via: config._pluginData.auth
83
+ context.setPluginData<NormalizedAuthConfig>('auth', normalized)
84
+ },
85
+ }
86
+ }
package/src/index.ts CHANGED
@@ -7,29 +7,30 @@
7
7
  * - Auto-generated User, Session, Account, Verification lists
8
8
  * - Session integration with OpenSaas access control
9
9
  * - Pre-built auth UI components (SignIn, SignUp, ForgotPassword)
10
- * - Easy configuration with withAuth() wrapper
10
+ * - Easy configuration with authPlugin()
11
11
  *
12
12
  * @example
13
13
  * ```typescript
14
14
  * // opensaas.config.ts
15
15
  * import { config } from '@opensaas/stack-core'
16
- * import { withAuth, authConfig } from '@opensaas/stack-auth'
16
+ * import { authPlugin } from '@opensaas/stack-auth'
17
17
  *
18
- * export default withAuth(
19
- * config({
20
- * db: { provider: 'sqlite', url: 'file:./dev.db' },
21
- * lists: { ... }
22
- * }),
23
- * authConfig({
24
- * emailAndPassword: { enabled: true },
25
- * emailVerification: { enabled: true },
26
- * })
27
- * )
18
+ * export default config({
19
+ * plugins: [
20
+ * authPlugin({
21
+ * emailAndPassword: { enabled: true },
22
+ * emailVerification: { enabled: true },
23
+ * })
24
+ * ],
25
+ * db: { provider: 'sqlite', url: 'file:./dev.db' },
26
+ * lists: { ... }
27
+ * })
28
28
  * ```
29
29
  */
30
30
 
31
31
  // Config exports
32
- export { withAuth, authConfig, normalizeAuthConfig } from './config/index.js'
32
+ export { normalizeAuthConfig } from './config/index.js'
33
+ export { authPlugin } from './config/plugin.js'
33
34
  export type { AuthConfig, NormalizedAuthConfig } from './config/index.js'
34
35
  export type * from './config/types.js'
35
36
 
@@ -93,7 +93,7 @@ export function withMcpAuth(
93
93
  * @example
94
94
  * ```typescript
95
95
  * const mcpSession = await auth.api.getMcpSession({ headers: req.headers })
96
- * const context = getContext(mcpSessionToContextSession(mcpSession))
96
+ * const context = await getContext(mcpSessionToContextSession(mcpSession))
97
97
  * const posts = await context.db.post.findMany()
98
98
  * ```
99
99
  */
@@ -25,65 +25,106 @@ function getDatabaseConfig(
25
25
  * // lib/auth.ts
26
26
  * import { createAuth } from '@opensaas/stack-auth/server'
27
27
  * import config from '../opensaas.config'
28
+ * import { rawOpensaasContext } from '@/.opensaas/context'
28
29
  *
29
- * export const auth = createAuth(config)
30
+ * export const auth = createAuth(config, rawOpensaasContext)
30
31
  * ```
31
32
  */
32
33
  export function createAuth(
33
- opensaasConfig: OpenSaasConfig & { __authConfig?: NormalizedAuthConfig },
34
- context: AccessContext,
34
+ opensaasConfig: OpenSaasConfig | Promise<OpenSaasConfig>,
35
+ context: AccessContext | Promise<AccessContext>,
35
36
  ) {
36
- // Extract auth config (added by withAuth)
37
- const authConfig = opensaasConfig.__authConfig
37
+ // Resolve config and context asynchronously
38
+ const configPromise = Promise.resolve(opensaasConfig)
39
+ const contextPromise = Promise.resolve(context)
38
40
 
39
- if (!authConfig) {
40
- throw new Error(
41
- 'Auth config not found. Make sure to wrap your config with withAuth() in opensaas.config.ts',
42
- )
43
- }
41
+ // Create auth instance lazily when needed
42
+ let authInstance: ReturnType<typeof betterAuth> | null = null
43
+ let authPromise: Promise<ReturnType<typeof betterAuth>> | null = null
44
+
45
+ async function getAuthInstance() {
46
+ if (authInstance) return authInstance
44
47
 
45
- // Build better-auth configuration
46
- const betterAuthConfig: BetterAuthOptions = {
47
- database: getDatabaseConfig(opensaasConfig.db, context),
48
+ if (!authPromise) {
49
+ authPromise = (async () => {
50
+ const resolvedConfig = await configPromise
51
+ const resolvedContext = await contextPromise
48
52
 
49
- // Enable email and password if configured
50
- emailAndPassword: authConfig.emailAndPassword.enabled
51
- ? {
52
- enabled: true,
53
- requireEmailVerification: authConfig.emailVerification.enabled,
53
+ // Extract auth config from plugin data
54
+ const authConfig = resolvedConfig._pluginData?.auth as NormalizedAuthConfig | undefined
55
+
56
+ if (!authConfig) {
57
+ throw new Error(
58
+ 'Auth config not found. Make sure to use authPlugin() in your opensaas.config.ts',
59
+ )
54
60
  }
55
- : undefined,
56
61
 
57
- // Configure session
58
- session: {
59
- expiresIn: authConfig.session.expiresIn || 604800,
60
- updateAge: authConfig.session.updateAge ? (authConfig.session.expiresIn || 604800) / 10 : 0,
61
- },
62
+ // Build better-auth configuration
63
+ const betterAuthConfig: BetterAuthOptions = {
64
+ database: getDatabaseConfig(resolvedConfig.db, resolvedContext),
65
+
66
+ // Enable email and password if configured
67
+ emailAndPassword: authConfig.emailAndPassword.enabled
68
+ ? {
69
+ enabled: true,
70
+ requireEmailVerification: authConfig.emailVerification.enabled,
71
+ }
72
+ : undefined,
73
+
74
+ // Configure session
75
+ session: {
76
+ expiresIn: authConfig.session.expiresIn || 604800,
77
+ updateAge: authConfig.session.updateAge
78
+ ? (authConfig.session.expiresIn || 604800) / 10
79
+ : 0,
80
+ },
81
+
82
+ // Trust host (required for production)
83
+ trustedOrigins: process.env.BETTER_AUTH_TRUSTED_ORIGINS?.split(',') || [],
84
+
85
+ // Social providers
86
+ socialProviders: Object.entries(authConfig.socialProviders)
87
+ .filter(([_, config]) => config?.enabled !== false)
88
+ .reduce(
89
+ (acc, [provider, config]) => {
90
+ if (config) {
91
+ acc[provider] = {
92
+ clientId: config.clientId,
93
+ clientSecret: config.clientSecret,
94
+ }
95
+ }
96
+ return acc
97
+ },
98
+ {} as Record<string, { clientId: string; clientSecret: string }>,
99
+ ),
100
+
101
+ // Pass through any additional Better Auth plugins
102
+ plugins: authConfig.betterAuthPlugins || [],
103
+ }
62
104
 
63
- // Trust host (required for production)
64
- trustedOrigins: process.env.BETTER_AUTH_TRUSTED_ORIGINS?.split(',') || [],
65
-
66
- // Social providers
67
- socialProviders: Object.entries(authConfig.socialProviders)
68
- .filter(([_, config]) => config?.enabled !== false)
69
- .reduce(
70
- (acc, [provider, config]) => {
71
- if (config) {
72
- acc[provider] = {
73
- clientId: config.clientId,
74
- clientSecret: config.clientSecret,
75
- }
76
- }
77
- return acc
78
- },
79
- {} as Record<string, { clientId: string; clientSecret: string }>,
80
- ),
105
+ authInstance = betterAuth(betterAuthConfig)
106
+ return authInstance
107
+ })()
108
+ }
81
109
 
82
- // Pass through any additional Better Auth plugins
83
- plugins: authConfig.betterAuthPlugins || [],
110
+ return authPromise
84
111
  }
85
112
 
86
- return betterAuth(betterAuthConfig)
113
+ // Return a proxy that lazily initializes the auth instance
114
+ return new Proxy({} as ReturnType<typeof betterAuth>, {
115
+ 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
+ }
126
+ },
127
+ })
87
128
  }
88
129
 
89
130
  /**