@objectstack/plugin-auth 4.0.4 → 4.1.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 (40) hide show
  1. package/README.md +4 -1
  2. package/dist/index.d.mts +441 -19940
  3. package/dist/index.d.ts +441 -19940
  4. package/dist/index.js +704 -900
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +699 -880
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +35 -12
  9. package/.turbo/turbo-build.log +0 -78
  10. package/ARCHITECTURE.md +0 -176
  11. package/CHANGELOG.md +0 -333
  12. package/IMPLEMENTATION_SUMMARY.md +0 -192
  13. package/examples/basic-usage.ts +0 -107
  14. package/objectstack.config.ts +0 -24
  15. package/src/auth-manager.test.ts +0 -883
  16. package/src/auth-manager.ts +0 -419
  17. package/src/auth-plugin.test.ts +0 -446
  18. package/src/auth-plugin.ts +0 -314
  19. package/src/auth-schema-config.ts +0 -339
  20. package/src/index.ts +0 -16
  21. package/src/objectql-adapter.test.ts +0 -281
  22. package/src/objectql-adapter.ts +0 -279
  23. package/src/objects/auth-account.object.ts +0 -7
  24. package/src/objects/auth-session.object.ts +0 -7
  25. package/src/objects/auth-user.object.ts +0 -7
  26. package/src/objects/auth-verification.object.ts +0 -7
  27. package/src/objects/index.ts +0 -40
  28. package/src/objects/sys-account.object.ts +0 -111
  29. package/src/objects/sys-api-key.object.ts +0 -104
  30. package/src/objects/sys-invitation.object.ts +0 -93
  31. package/src/objects/sys-member.object.ts +0 -68
  32. package/src/objects/sys-organization.object.ts +0 -82
  33. package/src/objects/sys-session.object.ts +0 -84
  34. package/src/objects/sys-team-member.object.ts +0 -61
  35. package/src/objects/sys-team.object.ts +0 -69
  36. package/src/objects/sys-two-factor.object.ts +0 -73
  37. package/src/objects/sys-user-preference.object.ts +0 -82
  38. package/src/objects/sys-user.object.ts +0 -91
  39. package/src/objects/sys-verification.object.ts +0 -75
  40. package/tsconfig.json +0 -18
@@ -1,419 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { betterAuth } from 'better-auth';
4
- import type { Auth, BetterAuthOptions } from 'better-auth';
5
- import { organization } from 'better-auth/plugins/organization';
6
- import { twoFactor } from 'better-auth/plugins/two-factor';
7
- import { magicLink } from 'better-auth/plugins/magic-link';
8
- import type {
9
- AuthConfig,
10
- EmailAndPasswordConfig,
11
- AuthPluginConfig,
12
- } from '@objectstack/spec/system';
13
- import type { IDataEngine } from '@objectstack/core';
14
- import { createObjectQLAdapterFactory } from './objectql-adapter.js';
15
- import {
16
- AUTH_USER_CONFIG,
17
- AUTH_SESSION_CONFIG,
18
- AUTH_ACCOUNT_CONFIG,
19
- AUTH_VERIFICATION_CONFIG,
20
- buildOrganizationPluginSchema,
21
- buildTwoFactorPluginSchema,
22
- } from './auth-schema-config.js';
23
-
24
- /**
25
- * Extended options for AuthManager
26
- */
27
- export interface AuthManagerOptions extends Partial<AuthConfig> {
28
- /**
29
- * Better-Auth instance (for advanced use cases)
30
- * If not provided, one will be created from config
31
- */
32
- authInstance?: Auth<any>;
33
-
34
- /**
35
- * ObjectQL Data Engine instance
36
- * Required for database operations using ObjectQL instead of third-party ORMs
37
- */
38
- dataEngine?: IDataEngine;
39
-
40
- /**
41
- * Base path for auth routes
42
- * Forwarded to better-auth's basePath option so it can match incoming
43
- * request URLs without manual path rewriting.
44
- * @default '/api/v1/auth'
45
- */
46
- basePath?: string;
47
- }
48
-
49
- /**
50
- * Authentication Manager
51
- *
52
- * Wraps better-auth and provides authentication services for ObjectStack.
53
- * Supports multiple authentication methods:
54
- * - Email/password
55
- * - OAuth providers (Google, GitHub, etc.)
56
- * - Magic links
57
- * - Two-factor authentication
58
- * - Passkeys
59
- * - Organization/teams
60
- */
61
- export class AuthManager {
62
- private auth: Auth<any> | null = null;
63
- private config: AuthManagerOptions;
64
-
65
- constructor(config: AuthManagerOptions) {
66
- this.config = config;
67
-
68
- // Use provided auth instance
69
- if (config.authInstance) {
70
- this.auth = config.authInstance;
71
- }
72
- // Don't create auth instance automatically to avoid database initialization errors
73
- // It will be created lazily when needed
74
- }
75
-
76
- /**
77
- * Get or create the better-auth instance (lazy initialization)
78
- */
79
- private getOrCreateAuth(): Auth<any> {
80
- if (!this.auth) {
81
- this.auth = this.createAuthInstance();
82
- }
83
- return this.auth;
84
- }
85
-
86
- /**
87
- * Create a better-auth instance from configuration
88
- */
89
- private createAuthInstance(): Auth<any> {
90
- const betterAuthConfig: BetterAuthOptions = {
91
- // Base configuration
92
- secret: this.config.secret || this.generateSecret(),
93
- baseURL: this.config.baseUrl || 'http://localhost:3000',
94
- basePath: this.config.basePath || '/api/v1/auth',
95
-
96
- // Database adapter configuration
97
- database: this.createDatabaseConfig(),
98
-
99
- // Model/field mapping: camelCase (better-auth) → snake_case (ObjectStack)
100
- // These declarations tell better-auth the actual table/column names used
101
- // by ObjectStack's protocol layer, enabling automatic transformation via
102
- // createAdapterFactory.
103
- user: {
104
- ...AUTH_USER_CONFIG,
105
- },
106
- account: {
107
- ...AUTH_ACCOUNT_CONFIG,
108
- },
109
- verification: {
110
- ...AUTH_VERIFICATION_CONFIG,
111
- },
112
-
113
- // Social / OAuth providers
114
- ...(this.config.socialProviders ? { socialProviders: this.config.socialProviders as any } : {}),
115
-
116
- // Email and password configuration
117
- emailAndPassword: {
118
- enabled: this.config.emailAndPassword?.enabled ?? true,
119
- ...(this.config.emailAndPassword?.disableSignUp != null
120
- ? { disableSignUp: this.config.emailAndPassword.disableSignUp } : {}),
121
- ...(this.config.emailAndPassword?.requireEmailVerification != null
122
- ? { requireEmailVerification: this.config.emailAndPassword.requireEmailVerification } : {}),
123
- ...(this.config.emailAndPassword?.minPasswordLength != null
124
- ? { minPasswordLength: this.config.emailAndPassword.minPasswordLength } : {}),
125
- ...(this.config.emailAndPassword?.maxPasswordLength != null
126
- ? { maxPasswordLength: this.config.emailAndPassword.maxPasswordLength } : {}),
127
- ...(this.config.emailAndPassword?.resetPasswordTokenExpiresIn != null
128
- ? { resetPasswordTokenExpiresIn: this.config.emailAndPassword.resetPasswordTokenExpiresIn } : {}),
129
- ...(this.config.emailAndPassword?.autoSignIn != null
130
- ? { autoSignIn: this.config.emailAndPassword.autoSignIn } : {}),
131
- ...(this.config.emailAndPassword?.revokeSessionsOnPasswordReset != null
132
- ? { revokeSessionsOnPasswordReset: this.config.emailAndPassword.revokeSessionsOnPasswordReset } : {}),
133
- },
134
-
135
- // Email verification
136
- ...(this.config.emailVerification ? {
137
- emailVerification: {
138
- ...(this.config.emailVerification.sendOnSignUp != null
139
- ? { sendOnSignUp: this.config.emailVerification.sendOnSignUp } : {}),
140
- ...(this.config.emailVerification.sendOnSignIn != null
141
- ? { sendOnSignIn: this.config.emailVerification.sendOnSignIn } : {}),
142
- ...(this.config.emailVerification.autoSignInAfterVerification != null
143
- ? { autoSignInAfterVerification: this.config.emailVerification.autoSignInAfterVerification } : {}),
144
- ...(this.config.emailVerification.expiresIn != null
145
- ? { expiresIn: this.config.emailVerification.expiresIn } : {}),
146
- },
147
- } : {}),
148
-
149
- // Session configuration
150
- session: {
151
- ...AUTH_SESSION_CONFIG,
152
- expiresIn: this.config.session?.expiresIn || 60 * 60 * 24 * 7, // 7 days default
153
- updateAge: this.config.session?.updateAge || 60 * 60 * 24, // 1 day default
154
- },
155
-
156
- // better-auth plugins — registered based on AuthPluginConfig flags
157
- plugins: this.buildPluginList(),
158
-
159
- // Trusted origins for CSRF protection (supports wildcards like "https://*.example.com")
160
- // Auto-includes origins from CORS_ORIGIN env var so CORS and CSRF stay in sync.
161
- ...(() => {
162
- const origins: string[] = [...(this.config.trustedOrigins || [])];
163
- // Sync with CORS_ORIGIN env var (comma-separated)
164
- const corsOrigin = process.env.CORS_ORIGIN;
165
- if (corsOrigin && corsOrigin !== '*') {
166
- corsOrigin.split(',').map(s => s.trim()).filter(Boolean).forEach(o => {
167
- if (!origins.includes(o)) origins.push(o);
168
- });
169
- }
170
- // When CORS allows all origins (default) and no explicit trustedOrigins,
171
- // trust all localhost ports in development for convenience.
172
- if (!origins.length && (!corsOrigin || corsOrigin === '*')) {
173
- origins.push('http://localhost:*');
174
- }
175
- return origins.length ? { trustedOrigins: origins } : {};
176
- })(),
177
-
178
- // Advanced options (cross-subdomain cookies, secure cookies, CSRF, etc.)
179
- ...(this.config.advanced ? {
180
- advanced: {
181
- ...(this.config.advanced.crossSubDomainCookies
182
- ? { crossSubDomainCookies: this.config.advanced.crossSubDomainCookies } : {}),
183
- ...(this.config.advanced.useSecureCookies != null
184
- ? { useSecureCookies: this.config.advanced.useSecureCookies } : {}),
185
- ...(this.config.advanced.disableCSRFCheck != null
186
- ? { disableCSRFCheck: this.config.advanced.disableCSRFCheck } : {}),
187
- ...(this.config.advanced.cookiePrefix != null
188
- ? { cookiePrefix: this.config.advanced.cookiePrefix } : {}),
189
- },
190
- } : {}),
191
- };
192
-
193
- return betterAuth(betterAuthConfig);
194
- }
195
-
196
- /**
197
- * Build the list of better-auth plugins based on AuthPluginConfig flags.
198
- *
199
- * Each plugin that introduces its own database tables is configured with
200
- * a `schema` option containing the appropriate snake_case field mappings,
201
- * so that `createAdapterFactory` transforms them automatically.
202
- */
203
- private buildPluginList(): any[] {
204
- const pluginConfig = this.config.plugins;
205
- const plugins: any[] = [];
206
-
207
- if (pluginConfig?.organization) {
208
- plugins.push(organization({
209
- schema: buildOrganizationPluginSchema(),
210
- }));
211
- }
212
-
213
- if (pluginConfig?.twoFactor) {
214
- plugins.push(twoFactor({
215
- schema: buildTwoFactorPluginSchema(),
216
- }));
217
- }
218
-
219
- if (pluginConfig?.magicLink) {
220
- // magic-link reuses the `verification` table — no extra schema mapping needed.
221
- // The sendMagicLink callback must be provided by the application at a higher level.
222
- // Here we provide a no-op default that logs a warning; real applications should
223
- // override this via AuthManagerOptions or a config extension point.
224
- plugins.push(magicLink({
225
- sendMagicLink: async ({ email, url }) => {
226
- console.warn(
227
- `[AuthManager] Magic-link requested for ${email} but no sendMagicLink handler configured. URL: ${url}`,
228
- );
229
- },
230
- }));
231
- }
232
-
233
- return plugins;
234
- }
235
-
236
- /**
237
- * Create database configuration using ObjectQL adapter
238
- *
239
- * better-auth resolves the `database` option as follows:
240
- * - `undefined` → in-memory adapter
241
- * - `typeof fn === "function"` → treated as `DBAdapterInstance`, called with `(options)`
242
- * - otherwise → forwarded to Kysely adapter factory (pool/dialect)
243
- *
244
- * A raw `CustomAdapter` object would fall into the third branch and fail
245
- * silently. We therefore wrap the ObjectQL adapter in a factory function
246
- * so it is correctly recognised as a `DBAdapterInstance`.
247
- */
248
- private createDatabaseConfig(): any {
249
- // Use ObjectQL adapter factory if dataEngine is provided
250
- if (this.config.dataEngine) {
251
- // createObjectQLAdapterFactory returns an AdapterFactory
252
- // (options => DBAdapter) which better-auth invokes via getBaseAdapter().
253
- // The factory is created by better-auth's createAdapterFactory and
254
- // automatically applies modelName/fields transformations declared in
255
- // the betterAuth config above.
256
- return createObjectQLAdapterFactory(this.config.dataEngine);
257
- }
258
-
259
- // Fallback warning if no dataEngine is provided
260
- console.warn(
261
- '⚠️ WARNING: No dataEngine provided to AuthManager! ' +
262
- 'Using in-memory storage. This is NOT suitable for production. ' +
263
- 'Please provide a dataEngine instance (e.g., ObjectQL) in AuthManagerOptions.'
264
- );
265
-
266
- // Return a minimal in-memory configuration as fallback
267
- // This allows the system to work in development/testing without a real database
268
- return undefined; // better-auth will use its default in-memory adapter
269
- }
270
-
271
- /**
272
- * Generate a secure secret if not provided
273
- */
274
- private generateSecret(): string {
275
- const envSecret = process.env.AUTH_SECRET;
276
-
277
- if (!envSecret) {
278
- // In production, a secret MUST be provided
279
- // For development/testing, we'll use a fallback but warn about it
280
- const fallbackSecret = 'dev-secret-' + Date.now();
281
-
282
- console.warn(
283
- '⚠️ WARNING: No AUTH_SECRET environment variable set! ' +
284
- 'Using a temporary development secret. ' +
285
- 'This is NOT secure for production use. ' +
286
- 'Please set AUTH_SECRET in your environment variables.'
287
- );
288
-
289
- return fallbackSecret;
290
- }
291
-
292
- return envSecret;
293
- }
294
-
295
- /**
296
- * Update the base URL at runtime.
297
- *
298
- * This **must** be called before the first request triggers lazy
299
- * initialisation of the better-auth instance — typically from a
300
- * `kernel:ready` hook where the actual server port is known.
301
- *
302
- * If the auth instance has already been created this is a no-op and
303
- * a warning is emitted.
304
- */
305
- setRuntimeBaseUrl(url: string): void {
306
- if (this.auth) {
307
- console.warn(
308
- '[AuthManager] setRuntimeBaseUrl() called after the auth instance was already created — ignoring. ' +
309
- 'Ensure this method is called before the first request.',
310
- );
311
- return;
312
- }
313
- this.config = { ...this.config, baseUrl: url };
314
- }
315
-
316
- /**
317
- * Get the underlying better-auth instance
318
- * Useful for advanced use cases
319
- */
320
- getAuthInstance(): Auth<any> {
321
- return this.getOrCreateAuth();
322
- }
323
-
324
- /**
325
- * Handle an authentication request
326
- * Forwards the request directly to better-auth's universal handler
327
- *
328
- * better-auth catches internal errors (database / adapter / ORM) and
329
- * returns a 500 Response instead of throwing. We therefore inspect the
330
- * response status and log server errors so they are not silently swallowed.
331
- *
332
- * @param request - Web standard Request object
333
- * @returns Web standard Response object
334
- */
335
- async handleRequest(request: Request): Promise<Response> {
336
- const auth = this.getOrCreateAuth();
337
- const response = await auth.handler(request);
338
-
339
- if (response.status >= 500) {
340
- try {
341
- const body = await response.clone().text();
342
- console.error('[AuthManager] better-auth returned error:', response.status, body);
343
- } catch {
344
- console.error('[AuthManager] better-auth returned error:', response.status, '(unable to read body)');
345
- }
346
- }
347
-
348
- return response;
349
- }
350
-
351
- /**
352
- * Get the better-auth API for programmatic access
353
- * Use this for server-side operations (e.g., creating users, checking sessions)
354
- */
355
- get api() {
356
- return this.getOrCreateAuth().api;
357
- }
358
-
359
- /**
360
- * Get public authentication configuration
361
- * Returns safe, non-sensitive configuration that can be exposed to the frontend
362
- *
363
- * This allows the frontend to discover:
364
- * - Which social/OAuth providers are available
365
- * - Whether email/password login is enabled
366
- * - Which advanced features are enabled (2FA, magic links, etc.)
367
- */
368
- getPublicConfig() {
369
- // Extract social providers info (without sensitive data)
370
- const socialProviders = [];
371
- if (this.config.socialProviders) {
372
- for (const [id, providerConfig] of Object.entries(this.config.socialProviders)) {
373
- if (providerConfig.enabled !== false) {
374
- // Map provider ID to friendly name
375
- const nameMap: Record<string, string> = {
376
- google: 'Google',
377
- github: 'GitHub',
378
- microsoft: 'Microsoft',
379
- apple: 'Apple',
380
- facebook: 'Facebook',
381
- twitter: 'Twitter',
382
- discord: 'Discord',
383
- gitlab: 'GitLab',
384
- linkedin: 'LinkedIn',
385
- };
386
-
387
- socialProviders.push({
388
- id,
389
- name: nameMap[id] || id.charAt(0).toUpperCase() + id.slice(1),
390
- enabled: true,
391
- });
392
- }
393
- }
394
- }
395
-
396
- // Extract email/password config (safe fields only)
397
- const emailPasswordConfig: Partial<EmailAndPasswordConfig> = this.config.emailAndPassword ?? {};
398
- const emailPassword = {
399
- enabled: emailPasswordConfig.enabled !== false, // Default to true
400
- disableSignUp: emailPasswordConfig.disableSignUp ?? false,
401
- requireEmailVerification: emailPasswordConfig.requireEmailVerification ?? false,
402
- };
403
-
404
- // Extract enabled features
405
- const pluginConfig: Partial<AuthPluginConfig> = this.config.plugins ?? {};
406
- const features = {
407
- twoFactor: pluginConfig.twoFactor ?? false,
408
- passkeys: pluginConfig.passkeys ?? false,
409
- magicLink: pluginConfig.magicLink ?? false,
410
- organization: pluginConfig.organization ?? false,
411
- };
412
-
413
- return {
414
- emailPassword,
415
- socialProviders,
416
- features,
417
- };
418
- }
419
- }