better-auth-studio 1.0.79-beta.1 → 1.0.79-beta.11

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 (74) hide show
  1. package/README.md +86 -0
  2. package/dist/adapters/express.d.ts +7 -0
  3. package/dist/adapters/express.d.ts.map +1 -0
  4. package/dist/adapters/express.js +40 -0
  5. package/dist/adapters/express.js.map +1 -0
  6. package/dist/adapters/nextjs.d.ts +3 -0
  7. package/dist/adapters/nextjs.d.ts.map +1 -0
  8. package/dist/adapters/nextjs.js +54 -0
  9. package/dist/adapters/nextjs.js.map +1 -0
  10. package/dist/auth-adapter.d.ts.map +1 -1
  11. package/dist/auth-adapter.js +3 -2
  12. package/dist/auth-adapter.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts +2 -0
  14. package/dist/cli/commands/init.d.ts.map +1 -0
  15. package/dist/cli/commands/init.js +140 -0
  16. package/dist/cli/commands/init.js.map +1 -0
  17. package/dist/cli.js +13 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +3 -0
  21. package/dist/config.js.map +1 -1
  22. package/dist/core/handler.d.ts +7 -0
  23. package/dist/core/handler.d.ts.map +1 -0
  24. package/dist/core/handler.js +384 -0
  25. package/dist/core/handler.js.map +1 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +3 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/public/assets/main-3NIBCudD.js +1155 -0
  31. package/dist/public/assets/main-DbXDm13A.css +1 -0
  32. package/dist/public/favicon.svg +6 -0
  33. package/dist/public/index.html +14 -0
  34. package/dist/public/logo.png +0 -0
  35. package/dist/public/vite.svg +5 -0
  36. package/dist/routes/api-router.d.ts +21 -0
  37. package/dist/routes/api-router.d.ts.map +1 -0
  38. package/dist/routes/api-router.js +14 -0
  39. package/dist/routes/api-router.js.map +1 -0
  40. package/dist/routes.d.ts +20 -1
  41. package/dist/routes.d.ts.map +1 -1
  42. package/dist/routes.js +681 -178
  43. package/dist/routes.js.map +1 -1
  44. package/dist/studio.d.ts.map +1 -1
  45. package/dist/studio.js +28 -2
  46. package/dist/studio.js.map +1 -1
  47. package/dist/types/handler.d.ts +58 -0
  48. package/dist/types/handler.d.ts.map +1 -0
  49. package/dist/types/handler.js +4 -0
  50. package/dist/types/handler.js.map +1 -0
  51. package/dist/utils/html-injector.d.ts +30 -0
  52. package/dist/utils/html-injector.d.ts.map +1 -0
  53. package/dist/utils/html-injector.js +61 -0
  54. package/dist/utils/html-injector.js.map +1 -0
  55. package/dist/utils/paths.d.ts +2 -0
  56. package/dist/utils/paths.d.ts.map +1 -0
  57. package/dist/utils/paths.js +12 -0
  58. package/dist/utils/paths.js.map +1 -0
  59. package/dist/utils/session.d.ts +21 -0
  60. package/dist/utils/session.d.ts.map +1 -0
  61. package/dist/utils/session.js +51 -0
  62. package/dist/utils/session.js.map +1 -0
  63. package/package.json +14 -5
  64. package/public/assets/main-3NIBCudD.js +1155 -0
  65. package/public/assets/main-DbXDm13A.css +1 -0
  66. package/public/favicon.svg +6 -0
  67. package/public/index.html +3 -3
  68. package/public/logo.png +0 -0
  69. package/public/vite.svg +5 -0
  70. package/scripts/download-geolite2.js +35 -0
  71. package/scripts/generate-default-db.js +462 -0
  72. package/scripts/postinstall.js +98 -0
  73. package/public/assets/main-Du6zwcd_.css +0 -1
  74. package/public/assets/main-S8anH3U1.js +0 -1145
package/dist/routes.js CHANGED
@@ -12,6 +12,7 @@ import { possiblePaths } from './config.js';
12
12
  import { getAuthData } from './data.js';
13
13
  import { initializeGeoService, resolveIPLocation, setGeoDbPath } from './geo-service.js';
14
14
  import { detectDatabaseWithDialect } from './utils/database-detection.js';
15
+ import { createStudioSession, decryptSession, encryptSession, isSessionValid, STUDIO_COOKIE_NAME, } from './utils/session.js';
15
16
  const config = {
16
17
  N: 16384,
17
18
  r: 16,
@@ -27,6 +28,27 @@ async function generateKey(password, salt) {
27
28
  maxmem: 128 * config.N * config.r * 2,
28
29
  });
29
30
  }
31
+ async function verifyPassword(password, storedHash) {
32
+ if (!storedHash || typeof storedHash !== 'string') {
33
+ return false;
34
+ }
35
+ const parts = storedHash.split(':');
36
+ if (parts.length !== 2) {
37
+ return false;
38
+ }
39
+ const [salt, storedKey] = parts;
40
+ if (!salt || !storedKey) {
41
+ return false;
42
+ }
43
+ try {
44
+ const key = await generateKey(password, salt);
45
+ const keyHex = hex.encode(key);
46
+ return keyHex === storedKey;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
30
52
  function getStudioVersion() {
31
53
  try {
32
54
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -227,7 +249,29 @@ async function findAuthConfigPath() {
227
249
  }
228
250
  return null;
229
251
  }
230
- export function createRoutes(authConfig, configPath, geoDbPath) {
252
+ export function createRoutes(authConfig, configPath, geoDbPath, preloadedAdapter, preloadedAuthOptions, accessConfig, authInstance) {
253
+ const isSelfHosted = !!preloadedAdapter;
254
+ const getAuthConfigSafe = async () => {
255
+ if (isSelfHosted && preloadedAuthOptions) {
256
+ return preloadedAuthOptions;
257
+ }
258
+ try {
259
+ const authConfigPath = configPath || (await findAuthConfigPath());
260
+ if (authConfigPath) {
261
+ const { getConfig } = await import('./config.js');
262
+ return await getConfig({
263
+ cwd: process.cwd(),
264
+ configPath: authConfigPath,
265
+ shouldThrowOnError: false,
266
+ noCache: true,
267
+ });
268
+ }
269
+ }
270
+ catch (_error) {
271
+ // Ignors errors
272
+ }
273
+ return null;
274
+ };
231
275
  const router = Router();
232
276
  const base64UrlEncode = (value) => Buffer.from(value)
233
277
  .toString('base64')
@@ -252,7 +296,105 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
252
296
  setGeoDbPath(geoDbPath);
253
297
  }
254
298
  initializeGeoService().catch(console.error);
255
- const getAuthAdapterWithConfig = () => getAuthAdapter(configPath);
299
+ // Use preloaded adapter if available (self-hosted), otherwise load from config file (CLI)
300
+ const getAuthAdapterWithConfig = async () => {
301
+ if (preloadedAdapter) {
302
+ // For self-hosted studio, wrap the preloaded adapter to match expected interface
303
+ return {
304
+ ...preloadedAdapter,
305
+ findMany: preloadedAdapter.findMany?.bind(preloadedAdapter),
306
+ create: preloadedAdapter.create?.bind(preloadedAdapter),
307
+ update: preloadedAdapter.update?.bind(preloadedAdapter),
308
+ delete: preloadedAdapter.delete?.bind(preloadedAdapter),
309
+ createUser: async (data) => {
310
+ return await preloadedAdapter.create({
311
+ model: 'user',
312
+ data: {
313
+ createdAt: new Date(),
314
+ updatedAt: new Date(),
315
+ emailVerified: false,
316
+ name: data.name,
317
+ email: data.email?.toLowerCase(),
318
+ role: data.role || null,
319
+ image: data.image || `https://api.dicebear.com/7.x/avataaars/svg?seed=${data.email}`,
320
+ },
321
+ });
322
+ },
323
+ createSession: async (data) => {
324
+ return await preloadedAdapter.create({
325
+ model: 'session',
326
+ data: { createdAt: new Date(), updatedAt: new Date(), ...data },
327
+ });
328
+ },
329
+ createAccount: async (data) => {
330
+ return await preloadedAdapter.create({
331
+ model: 'account',
332
+ data: { createdAt: new Date(), updatedAt: new Date(), ...data },
333
+ });
334
+ },
335
+ createVerification: async (data) => {
336
+ return await preloadedAdapter.create({
337
+ model: 'verification',
338
+ data: { createdAt: new Date(), updatedAt: new Date(), ...data },
339
+ });
340
+ },
341
+ createOrganization: async (data) => {
342
+ return await preloadedAdapter.create({
343
+ model: 'organization',
344
+ data: { createdAt: new Date(), updatedAt: new Date(), ...data },
345
+ });
346
+ },
347
+ getUsers: async () => {
348
+ try {
349
+ if (typeof preloadedAdapter.findMany === 'function') {
350
+ return (await preloadedAdapter.findMany({ model: 'user' })) || [];
351
+ }
352
+ return [];
353
+ }
354
+ catch {
355
+ return [];
356
+ }
357
+ },
358
+ getSessions: async () => {
359
+ try {
360
+ if (typeof preloadedAdapter.findMany === 'function') {
361
+ return (await preloadedAdapter.findMany({ model: 'session' })) || [];
362
+ }
363
+ return [];
364
+ }
365
+ catch {
366
+ return [];
367
+ }
368
+ },
369
+ };
370
+ }
371
+ return getAuthAdapter(configPath);
372
+ };
373
+ if (isSelfHosted) {
374
+ router.use((req, res, next) => {
375
+ const path = req.path;
376
+ const publicPaths = [
377
+ '/api/auth/sign-in',
378
+ '/api/auth/session',
379
+ '/api/auth/logout',
380
+ '/api/auth/verify',
381
+ '/api/auth/oauth',
382
+ '/api/health',
383
+ ];
384
+ const isPublic = publicPaths.some((p) => path.startsWith(p));
385
+ if (isPublic) {
386
+ return next();
387
+ }
388
+ if (path.startsWith('/api/')) {
389
+ const result = verifyStudioSession(req);
390
+ if (!result.valid) {
391
+ return res.status(401).json({ error: 'Unauthorized', message: result.error });
392
+ }
393
+ req.studioSession = result.session;
394
+ }
395
+ next();
396
+ });
397
+ }
256
398
  router.get('/api/health', (_req, res) => {
257
399
  const uptime = process.uptime();
258
400
  const hours = Math.floor(uptime / 3600);
@@ -277,6 +419,285 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
277
419
  },
278
420
  });
279
421
  });
422
+ const getSessionSecret = () => {
423
+ return (accessConfig?.secret ||
424
+ preloadedAuthOptions?.secret ||
425
+ process.env.BETTER_AUTH_SECRET ||
426
+ 'studio-default-secret');
427
+ };
428
+ const getAllowedRoles = () => {
429
+ return accessConfig?.roles || ['admin'];
430
+ };
431
+ const getSessionDuration = () => {
432
+ return (accessConfig?.sessionDuration || 7 * 24 * 60 * 60) * 1000;
433
+ };
434
+ const getAllowedEmails = () => {
435
+ return accessConfig?.allowEmails && accessConfig.allowEmails.length > 0
436
+ ? accessConfig.allowEmails.map((e) => e.toLowerCase())
437
+ : null;
438
+ };
439
+ const isEmailAllowed = (email) => {
440
+ const allowedEmails = getAllowedEmails();
441
+ if (!allowedEmails)
442
+ return true;
443
+ return allowedEmails.includes(email.toLowerCase());
444
+ };
445
+ const verifyStudioSession = (req) => {
446
+ if (!isSelfHosted) {
447
+ return { valid: true };
448
+ }
449
+ const sessionCookie = req.cookies?.[STUDIO_COOKIE_NAME];
450
+ if (!sessionCookie) {
451
+ return { valid: false, error: 'No session cookie' };
452
+ }
453
+ const session = decryptSession(sessionCookie, getSessionSecret());
454
+ if (!isSessionValid(session)) {
455
+ return { valid: false, error: 'Session expired' };
456
+ }
457
+ return { valid: true, session };
458
+ };
459
+ const requireAuth = (req, res, next) => {
460
+ if (!isSelfHosted) {
461
+ return next();
462
+ }
463
+ const result = verifyStudioSession(req);
464
+ if (!result.valid) {
465
+ return res.status(401).json({ error: 'Unauthorized', message: result.error });
466
+ }
467
+ req.studioSession = result.session;
468
+ next();
469
+ };
470
+ router.post('/api/auth/sign-in', async (req, res) => {
471
+ try {
472
+ if (!authInstance) {
473
+ return res.status(500).json({ success: false, message: 'Auth not configured' });
474
+ }
475
+ const { email, password } = req.body;
476
+ if (!email || !password) {
477
+ return res.status(400).json({ success: false, message: 'Email and password required' });
478
+ }
479
+ const adapter = await getAuthAdapter();
480
+ let signInResult = null;
481
+ let signInError = null;
482
+ try {
483
+ signInResult = await authInstance.api.signInEmail({
484
+ body: { email, password },
485
+ });
486
+ }
487
+ catch (err) {
488
+ signInError = err?.message || 'Sign-in failed';
489
+ }
490
+ if (!signInResult || signInResult.error || signInError) {
491
+ const errorMessage = signInError || signInResult?.error?.message || 'Invalid credentials';
492
+ if (errorMessage.includes('Invalid password hash') && adapter?.findMany) {
493
+ const users = await adapter.findMany({
494
+ model: 'user',
495
+ where: [{ field: 'email', value: email }],
496
+ limit: 1,
497
+ });
498
+ if (!users || users.length === 0) {
499
+ return res.status(401).json({ success: false, message: 'Invalid credentials' });
500
+ }
501
+ const userId = users[0].id;
502
+ const accounts = await adapter.findMany({
503
+ model: 'account',
504
+ where: [{ field: 'userId', value: userId }],
505
+ limit: 10,
506
+ });
507
+ const credentialAccount = accounts?.find((acc) => acc.providerId === 'credential' || acc.providerId === 'email');
508
+ if (!credentialAccount) {
509
+ return res.status(401).json({
510
+ success: false,
511
+ message: 'No password set for this account. Please use social login or reset your password.',
512
+ });
513
+ }
514
+ if (!credentialAccount.password) {
515
+ return res.status(401).json({
516
+ success: false,
517
+ message: 'Password not configured. Please reset your password.',
518
+ });
519
+ }
520
+ console.log({ users, credentialAccount });
521
+ const isValidPassword = await verifyPassword(password, credentialAccount.password);
522
+ if (!isValidPassword) {
523
+ return res.status(401).json({ success: false, message: 'Invalid credentials' });
524
+ }
525
+ const userRole = users[0].role;
526
+ const user = { id: userId, email: users[0].email, name: users[0].name, role: userRole };
527
+ const allowedRoles = getAllowedRoles();
528
+ if (!allowedRoles.includes(user.role)) {
529
+ return res.status(403).json({
530
+ success: false,
531
+ message: `Access denied. Required role: ${allowedRoles.join(' or ')}`,
532
+ userRole: user.role || 'none',
533
+ });
534
+ }
535
+ if (!isEmailAllowed(user.email)) {
536
+ return res.status(403).json({
537
+ success: false,
538
+ message: 'Access denied. Your email is not authorized to access this dashboard.',
539
+ });
540
+ }
541
+ const studioSession = createStudioSession(user, getSessionDuration());
542
+ const encryptedSession = encryptSession(studioSession, getSessionSecret());
543
+ res.cookie(STUDIO_COOKIE_NAME, encryptedSession, {
544
+ httpOnly: true,
545
+ secure: process.env.NODE_ENV === 'production',
546
+ sameSite: 'lax',
547
+ maxAge: getSessionDuration(),
548
+ path: '/',
549
+ });
550
+ return res.json({
551
+ success: true,
552
+ user: { id: user.id, email: user.email, name: user.name, role: user.role },
553
+ });
554
+ }
555
+ return res.status(401).json({
556
+ success: false,
557
+ message: errorMessage,
558
+ });
559
+ }
560
+ const userId = signInResult.user?.id;
561
+ if (!userId) {
562
+ return res.status(401).json({ success: false, message: 'Invalid credentials' });
563
+ }
564
+ let userRole = null;
565
+ if (adapter?.findMany) {
566
+ const users = await adapter.findMany({
567
+ model: 'user',
568
+ where: [{ field: 'id', value: userId }],
569
+ limit: 1,
570
+ });
571
+ if (users && users.length > 0) {
572
+ userRole = users[0].role;
573
+ }
574
+ }
575
+ const user = { ...signInResult.user, role: userRole };
576
+ const allowedRoles = getAllowedRoles();
577
+ if (!allowedRoles.includes(user.role)) {
578
+ return res.status(403).json({
579
+ success: false,
580
+ message: `Access denied. Required role: ${allowedRoles.join(' or ')}`,
581
+ userRole: user.role || 'none',
582
+ });
583
+ }
584
+ if (!isEmailAllowed(user.email)) {
585
+ return res.status(403).json({
586
+ success: false,
587
+ message: 'Access denied. Your email is not authorized to access this dashboard.',
588
+ });
589
+ }
590
+ const studioSession = createStudioSession(user, getSessionDuration());
591
+ const encryptedSession = encryptSession(studioSession, getSessionSecret());
592
+ res.cookie(STUDIO_COOKIE_NAME, encryptedSession, {
593
+ httpOnly: true,
594
+ secure: process.env.NODE_ENV === 'production',
595
+ sameSite: 'lax',
596
+ maxAge: getSessionDuration(),
597
+ path: '/',
598
+ });
599
+ return res.json({
600
+ success: true,
601
+ user: { id: user.id, email: user.email, name: user.name, role: user.role },
602
+ });
603
+ }
604
+ catch (error) {
605
+ console.error('Sign-in error:', error);
606
+ return res.status(401).json({
607
+ success: false,
608
+ message: error?.message || 'Invalid credentials',
609
+ });
610
+ }
611
+ });
612
+ router.get('/api/auth/oauth/:provider', async (req, res) => {
613
+ try {
614
+ if (!authInstance) {
615
+ return res.status(500).json({ success: false, message: 'Auth not configured' });
616
+ }
617
+ const provider = req.params.provider;
618
+ const callbackURL = req.query.callbackURL;
619
+ const authBasePath = authInstance.options?.basePath || '/api/auth';
620
+ const oauthUrl = `${authBasePath}/sign-in/${provider}?callbackURL=${encodeURIComponent(callbackURL || '/')}`;
621
+ return res.redirect(oauthUrl);
622
+ }
623
+ catch (error) {
624
+ console.error('OAuth redirect error:', error);
625
+ return res.status(500).json({ success: false, message: 'OAuth redirect failed' });
626
+ }
627
+ });
628
+ router.post('/api/auth/verify', async (req, res) => {
629
+ try {
630
+ if (!authInstance) {
631
+ return res.status(500).json({ success: false, message: 'Auth not configured' });
632
+ }
633
+ const session = await authInstance.api.getSession({ headers: req.headers });
634
+ if (!session?.user) {
635
+ return res.status(401).json({ success: false, message: 'Not authenticated' });
636
+ }
637
+ const user = session.user;
638
+ const allowedRoles = getAllowedRoles();
639
+ if (!allowedRoles.includes(user.role)) {
640
+ return res.status(403).json({
641
+ success: false,
642
+ message: `Access denied. Required role: ${allowedRoles.join(' or ')}`,
643
+ userRole: user.role || 'none',
644
+ });
645
+ }
646
+ if (!isEmailAllowed(user.email)) {
647
+ return res.status(403).json({
648
+ success: false,
649
+ message: 'Access denied. Your email is not authorized to access this dashboard.',
650
+ });
651
+ }
652
+ const studioSession = createStudioSession(user, getSessionDuration());
653
+ const encryptedSession = encryptSession(studioSession, getSessionSecret());
654
+ res.cookie(STUDIO_COOKIE_NAME, encryptedSession, {
655
+ httpOnly: true,
656
+ secure: process.env.NODE_ENV === 'production',
657
+ sameSite: 'lax',
658
+ maxAge: getSessionDuration(),
659
+ path: '/',
660
+ });
661
+ return res.json({
662
+ success: true,
663
+ user: { id: user.id, email: user.email, name: user.name, role: user.role },
664
+ });
665
+ }
666
+ catch (error) {
667
+ console.error('Auth verify error:', error);
668
+ return res.status(500).json({ success: false, message: 'Failed to verify session' });
669
+ }
670
+ });
671
+ router.get('/api/auth/session', (req, res) => {
672
+ const sessionCookie = req.cookies?.[STUDIO_COOKIE_NAME];
673
+ if (!sessionCookie) {
674
+ return res.json({ authenticated: false });
675
+ }
676
+ const session = decryptSession(sessionCookie, getSessionSecret());
677
+ if (!isSessionValid(session)) {
678
+ return res.json({ authenticated: false, reason: 'expired' });
679
+ }
680
+ return res.json({
681
+ authenticated: true,
682
+ user: {
683
+ id: session.userId,
684
+ email: session.email,
685
+ name: session.name,
686
+ role: session.role,
687
+ image: session.image,
688
+ },
689
+ });
690
+ });
691
+ router.get('/api/auth/logout', (_req, res) => {
692
+ res.cookie(STUDIO_COOKIE_NAME, '', {
693
+ httpOnly: true,
694
+ secure: process.env.NODE_ENV === 'production',
695
+ sameSite: 'lax',
696
+ maxAge: 0,
697
+ path: '/',
698
+ });
699
+ return res.json({ success: true, message: 'Logged out' });
700
+ });
280
701
  router.get('/api/version-check', async (_req, res) => {
281
702
  try {
282
703
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -361,6 +782,7 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
361
782
  }
362
783
  });
363
784
  router.get('/api/config', async (_req, res) => {
785
+ const effectiveConfig = preloadedAuthOptions || authConfig;
364
786
  let databaseType = 'unknown';
365
787
  let databaseDialect = 'unknown';
366
788
  let databaseAdapter = 'unknown';
@@ -385,22 +807,25 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
385
807
  }
386
808
  }
387
809
  catch (_error) { }
388
- if (databaseType === 'unknown') {
389
- const configPath = await findAuthConfigPath();
390
- if (configPath) {
391
- const content = readFileSync(configPath, 'utf-8');
392
- if (content.includes('drizzleAdapter')) {
393
- databaseType = 'Drizzle';
394
- }
395
- else if (content.includes('prismaAdapter')) {
396
- databaseType = 'Prisma';
397
- }
398
- else if (content.includes('better-sqlite3') || content.includes('new Database(')) {
399
- databaseType = 'SQLite';
810
+ if (databaseType === 'unknown' && !isSelfHosted) {
811
+ const authConfigPath = configPath || (await findAuthConfigPath());
812
+ if (authConfigPath) {
813
+ try {
814
+ const content = readFileSync(authConfigPath, 'utf-8');
815
+ if (content.includes('drizzleAdapter')) {
816
+ databaseType = 'Drizzle';
817
+ }
818
+ else if (content.includes('prismaAdapter')) {
819
+ databaseType = 'Prisma';
820
+ }
821
+ else if (content.includes('better-sqlite3') || content.includes('new Database(')) {
822
+ databaseType = 'SQLite';
823
+ }
400
824
  }
825
+ catch (_error) { }
401
826
  }
402
827
  if (databaseType === 'unknown') {
403
- let type = authConfig.database?.type || authConfig.database?.adapter || 'unknown';
828
+ let type = effectiveConfig.database?.type || effectiveConfig.database?.adapter || 'unknown';
404
829
  if (type && type !== 'unknown') {
405
830
  type = type.charAt(0).toUpperCase() + type.slice(1);
406
831
  }
@@ -408,52 +833,55 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
408
833
  }
409
834
  }
410
835
  const config = {
411
- appName: authConfig.appName || 'Better Auth',
412
- baseURL: authConfig.baseURL || process.env.BETTER_AUTH_URL,
413
- basePath: authConfig.basePath || '/api/auth',
414
- secret: authConfig.secret ? 'Configured' : 'Not set',
836
+ appName: effectiveConfig.appName || 'Better Auth',
837
+ baseURL: effectiveConfig.baseURL || process.env.BETTER_AUTH_URL,
838
+ basePath: effectiveConfig.basePath || '/api/auth',
839
+ secret: effectiveConfig.secret ? 'Configured' : 'Not set',
415
840
  database: {
416
841
  type: databaseType,
417
- adapter: authConfig.database?.adapter || databaseAdapter,
842
+ adapter: effectiveConfig.database?.adapter || databaseAdapter,
418
843
  version: databaseVersion,
419
- casing: authConfig.database?.casing || 'camel',
420
- debugLogs: authConfig.database?.debugLogs || false,
421
- url: authConfig.database?.url,
844
+ casing: effectiveConfig.database?.casing || 'camel',
845
+ debugLogs: effectiveConfig.database?.debugLogs || false,
846
+ url: effectiveConfig.database?.url,
422
847
  adapterConfig: adapterConfig,
423
848
  dialect: adapterProvider,
424
849
  },
425
- emailVerification: authConfig.emailVerification,
426
- emailAndPassword: authConfig.emailAndPassword,
427
- socialProviders: authConfig.socialProviders
428
- ? authConfig.socialProviders.map((provider) => ({
429
- type: provider.id,
850
+ emailVerification: effectiveConfig.emailVerification,
851
+ emailAndPassword: effectiveConfig.emailAndPassword,
852
+ socialProviders: effectiveConfig.socialProviders
853
+ ? Object.entries(effectiveConfig.socialProviders).map(([id, provider]) => ({
854
+ type: id,
430
855
  clientId: provider.clientId,
431
856
  clientSecret: provider.clientSecret,
432
- redirectUri: provider.redirectUri,
857
+ id: id,
858
+ name: id,
859
+ redirectURI: provider.redirectURI,
860
+ enabled: !!(provider.clientId && provider.clientSecret),
433
861
  ...provider,
434
862
  }))
435
- : authConfig.providers || [],
863
+ : [],
436
864
  user: {
437
- modelName: authConfig.user?.modelName || 'user',
865
+ modelName: effectiveConfig.user?.modelName || 'user',
438
866
  changeEmail: {
439
- enabled: authConfig.user?.changeEmail?.enabled || false,
867
+ enabled: effectiveConfig.user?.changeEmail?.enabled || false,
440
868
  },
441
869
  deleteUser: {
442
- enabled: authConfig.user?.deleteUser?.enabled || false,
443
- deleteTokenExpiresIn: authConfig.user?.deleteUser?.deleteTokenExpiresIn || 86400,
870
+ enabled: effectiveConfig.user?.deleteUser?.enabled || false,
871
+ deleteTokenExpiresIn: effectiveConfig.user?.deleteUser?.deleteTokenExpiresIn || 86400,
444
872
  },
445
873
  },
446
- session: authConfig.session,
447
- account: authConfig.account,
874
+ session: effectiveConfig.session,
875
+ account: effectiveConfig.account,
448
876
  verification: {
449
- modelName: authConfig.verification?.modelName || 'verification',
450
- disableCleanup: authConfig.verification?.disableCleanup || false,
877
+ modelName: effectiveConfig.verification?.modelName || 'verification',
878
+ disableCleanup: effectiveConfig.verification?.disableCleanup || false,
451
879
  },
452
- trustedOrigins: authConfig.trustedOrigins,
453
- rateLimit: authConfig.rateLimit,
454
- advanced: authConfig.advanced,
455
- disabledPaths: authConfig.disabledPaths || [],
456
- telemetry: authConfig.telemetry,
880
+ trustedOrigins: effectiveConfig.trustedOrigins,
881
+ rateLimit: effectiveConfig.rateLimit,
882
+ advanced: effectiveConfig.advanced,
883
+ disabledPaths: effectiveConfig.disabledPaths || [],
884
+ telemetry: effectiveConfig.telemetry,
457
885
  studio: {
458
886
  version: getStudioVersion(),
459
887
  nodeVersion: process.version,
@@ -497,23 +925,26 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
497
925
  let organizationPluginEnabled = false;
498
926
  let teamsPluginEnabled = false;
499
927
  try {
500
- const authConfigPath = configPath || (await findAuthConfigPath());
501
- if (authConfigPath) {
502
- const { getConfig } = await import('./config.js');
503
- const betterAuthConfig = await getConfig({
504
- cwd: process.cwd(),
505
- configPath: authConfigPath,
506
- shouldThrowOnError: false,
507
- noCache: true, // Disable cache for real-time plugin checks
508
- });
509
- if (betterAuthConfig) {
510
- const plugins = betterAuthConfig.plugins || [];
511
- const organizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
512
- organizationPluginEnabled = !!organizationPlugin;
513
- teamsPluginEnabled = !!organizationPlugin?.options?.teams?.enabled;
514
- if (organizationPlugin) {
515
- teamsPluginEnabled = organizationPlugin.options?.teams?.enabled === true;
516
- }
928
+ let betterAuthConfig = preloadedAuthOptions;
929
+ if (!betterAuthConfig && !isSelfHosted) {
930
+ const authConfigPath = configPath || (await findAuthConfigPath());
931
+ if (authConfigPath) {
932
+ const { getConfig } = await import('./config.js');
933
+ betterAuthConfig = await getConfig({
934
+ cwd: process.cwd(),
935
+ configPath: authConfigPath,
936
+ shouldThrowOnError: false,
937
+ noCache: true, // Disable cache for real-time plugin checks
938
+ });
939
+ }
940
+ }
941
+ if (betterAuthConfig) {
942
+ const plugins = betterAuthConfig.plugins || [];
943
+ const organizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
944
+ organizationPluginEnabled = !!organizationPlugin;
945
+ teamsPluginEnabled = !!organizationPlugin?.options?.teams?.enabled;
946
+ if (organizationPlugin) {
947
+ teamsPluginEnabled = organizationPlugin.options?.teams?.enabled === true;
517
948
  }
518
949
  }
519
950
  }
@@ -1109,7 +1540,6 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1109
1540
  authModule = await safeImportAuthConfig(authConfigPath, true); // Disable cache for real-time plugin checks
1110
1541
  }
1111
1542
  catch (_importError) {
1112
- // Fallback: read file content directly
1113
1543
  const content = readFileSync(authConfigPath, 'utf-8');
1114
1544
  authModule = {
1115
1545
  auth: {
@@ -1407,13 +1837,22 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1407
1837
  addResult('Database', 'Connection String', 'pass', 'Database connection string is configured');
1408
1838
  }
1409
1839
  // 3. OAuth/Social Providers
1410
- const socialProviders = authConfig.socialProviders || [];
1411
- if (socialProviders.length === 0) {
1840
+ const socialProvidersRaw = (preloadedAuthOptions || authConfig || {}).socialProviders || {};
1841
+ const effectiveSocialProviders = Array.isArray(socialProvidersRaw)
1842
+ ? socialProvidersRaw
1843
+ : Object.entries(socialProvidersRaw).map(([id, p]) => ({
1844
+ id,
1845
+ type: id,
1846
+ name: id,
1847
+ ...p,
1848
+ enabled: !!(p.clientId && p.clientSecret),
1849
+ }));
1850
+ if (effectiveSocialProviders.length === 0) {
1412
1851
  addResult('OAuth Providers', 'Providers', 'warning', 'No OAuth providers configured', 'This is optional. Add social providers if you need OAuth authentication', 'info');
1413
1852
  }
1414
1853
  else {
1415
- addResult('OAuth Providers', 'Providers', 'pass', `${socialProviders.length} OAuth provider(s) configured`);
1416
- socialProviders.forEach((provider) => {
1854
+ addResult('OAuth Providers', 'Providers', 'pass', `${effectiveSocialProviders.length} OAuth provider(s) configured`);
1855
+ effectiveSocialProviders.forEach((provider) => {
1417
1856
  if (provider.enabled) {
1418
1857
  if (!provider.clientId) {
1419
1858
  addResult('OAuth Providers', `${provider.name} - Client ID`, 'fail', `${provider.name} is enabled but clientId is missing`, `Add clientId for ${provider.name} in your auth config`, 'error');
@@ -1608,23 +2047,11 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1608
2047
  });
1609
2048
  router.post('/api/admin/ban-user', async (req, res) => {
1610
2049
  try {
1611
- const authConfigPath = configPath || (await findAuthConfigPath());
1612
- if (!authConfigPath) {
1613
- return res.status(400).json({
1614
- success: false,
1615
- error: 'No auth config found',
1616
- });
1617
- }
1618
- const { getConfig } = await import('./config.js');
1619
- const auth = await getConfig({
1620
- cwd: process.cwd(),
1621
- configPath: authConfigPath,
1622
- shouldThrowOnError: false,
1623
- });
2050
+ const auth = await getAuthConfigSafe();
1624
2051
  if (!auth) {
1625
2052
  return res.status(400).json({
1626
2053
  success: false,
1627
- error: 'Failed to load auth config',
2054
+ error: 'No auth config found',
1628
2055
  });
1629
2056
  }
1630
2057
  const plugins = auth.plugins || [];
@@ -1659,23 +2086,11 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1659
2086
  });
1660
2087
  router.post('/api/admin/unban-user', async (req, res) => {
1661
2088
  try {
1662
- const authConfigPath = configPath || (await findAuthConfigPath());
1663
- if (!authConfigPath) {
1664
- return res.status(400).json({
1665
- success: false,
1666
- error: 'No auth config found',
1667
- });
1668
- }
1669
- const { getConfig } = await import('./config.js');
1670
- const auth = await getConfig({
1671
- cwd: process.cwd(),
1672
- configPath: authConfigPath,
1673
- shouldThrowOnError: false,
1674
- });
2089
+ const auth = await getAuthConfigSafe();
1675
2090
  if (!auth) {
1676
2091
  return res.status(400).json({
1677
2092
  success: false,
1678
- error: 'Failed to load auth config',
2093
+ error: 'No auth config found',
1679
2094
  });
1680
2095
  }
1681
2096
  const plugins = auth.plugins || [];
@@ -1710,32 +2125,19 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1710
2125
  });
1711
2126
  router.get('/api/admin/status', async (_req, res) => {
1712
2127
  try {
1713
- const authConfigPath = configPath || (await findAuthConfigPath());
1714
- if (!authConfigPath) {
2128
+ const betterAuthConfig = await getAuthConfigSafe();
2129
+ if (!betterAuthConfig) {
1715
2130
  return res.json({
1716
2131
  enabled: false,
1717
2132
  error: 'No auth config found',
1718
2133
  configPath: null,
1719
2134
  });
1720
2135
  }
1721
- const { getConfig } = await import('./config.js');
1722
- const betterAuthConfig = await getConfig({
1723
- cwd: process.cwd(),
1724
- configPath: authConfigPath,
1725
- shouldThrowOnError: false,
1726
- });
1727
- if (!betterAuthConfig) {
1728
- return res.json({
1729
- enabled: false,
1730
- error: 'Failed to load auth config',
1731
- configPath: authConfigPath,
1732
- });
1733
- }
1734
2136
  const plugins = betterAuthConfig.plugins || [];
1735
2137
  const adminPlugin = plugins.find((plugin) => plugin.id === 'admin');
1736
2138
  res.json({
1737
2139
  enabled: !!adminPlugin,
1738
- configPath: authConfigPath,
2140
+ configPath: configPath || null,
1739
2141
  adminPlugin: adminPlugin || null,
1740
2142
  });
1741
2143
  }
@@ -1950,51 +2352,32 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1950
2352
  });
1951
2353
  router.get('/api/plugins/teams/status', async (_req, res) => {
1952
2354
  try {
1953
- const authConfigPath = configPath || (await findAuthConfigPath());
1954
- if (!authConfigPath) {
2355
+ const betterAuthConfig = await getAuthConfigSafe();
2356
+ if (!betterAuthConfig) {
1955
2357
  return res.json({
1956
2358
  enabled: false,
1957
2359
  error: 'No auth config found',
1958
2360
  configPath: null,
1959
2361
  });
1960
2362
  }
1961
- try {
1962
- const { getConfig } = await import('./config.js');
1963
- const betterAuthConfig = await getConfig({
1964
- cwd: process.cwd(),
1965
- configPath: authConfigPath,
1966
- shouldThrowOnError: false,
1967
- noCache: true, // Disable cache for real-time plugin status checks
2363
+ const plugins = betterAuthConfig.plugins || [];
2364
+ const organizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
2365
+ if (organizationPlugin) {
2366
+ const teamsEnabled = organizationPlugin.options?.teams?.enabled === true;
2367
+ return res.json({
2368
+ enabled: teamsEnabled,
2369
+ configPath: configPath || null,
2370
+ organizationPlugin: organizationPlugin || null,
1968
2371
  });
1969
- if (betterAuthConfig) {
1970
- const plugins = betterAuthConfig.plugins || [];
1971
- const organizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
1972
- if (organizationPlugin) {
1973
- const teamsEnabled = organizationPlugin.options?.teams?.enabled === true;
1974
- return res.json({
1975
- enabled: teamsEnabled,
1976
- configPath: authConfigPath,
1977
- organizationPlugin: organizationPlugin || null,
1978
- });
1979
- }
1980
- else {
1981
- return res.json({
1982
- enabled: false,
1983
- configPath: authConfigPath,
1984
- organizationPlugin: null,
1985
- error: 'Organization plugin not found',
1986
- });
1987
- }
1988
- }
1989
- res.json({
2372
+ }
2373
+ else {
2374
+ return res.json({
1990
2375
  enabled: false,
1991
- error: 'Failed to load auth config - getConfig failed and regex extraction unavailable',
1992
- configPath: authConfigPath,
2376
+ configPath: configPath || null,
2377
+ organizationPlugin: null,
2378
+ error: 'Organization plugin not found',
1993
2379
  });
1994
2380
  }
1995
- catch (_error) {
1996
- res.status(500).json({ error: 'Failed to check teams status' });
1997
- }
1998
2381
  }
1999
2382
  catch (_error) {
2000
2383
  res.status(500).json({ error: 'Failed to check teams status' });
@@ -2591,41 +2974,22 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
2591
2974
  });
2592
2975
  router.get('/api/plugins/organization/status', async (_req, res) => {
2593
2976
  try {
2594
- const authConfigPath = configPath || (await findAuthConfigPath());
2595
- if (!authConfigPath) {
2977
+ const betterAuthConfig = await getAuthConfigSafe();
2978
+ if (!betterAuthConfig) {
2596
2979
  return res.json({
2597
2980
  enabled: false,
2598
2981
  error: 'No auth config found',
2599
2982
  configPath: null,
2600
2983
  });
2601
2984
  }
2602
- try {
2603
- const { getConfig } = await import('./config.js');
2604
- const betterAuthConfig = await getConfig({
2605
- cwd: process.cwd(),
2606
- configPath: authConfigPath,
2607
- shouldThrowOnError: false,
2608
- noCache: true,
2609
- });
2610
- if (betterAuthConfig) {
2611
- const plugins = betterAuthConfig?.plugins || [];
2612
- const hasOrganizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
2613
- return res.json({
2614
- enabled: !!hasOrganizationPlugin,
2615
- configPath: authConfigPath,
2616
- availablePlugins: plugins.map((p) => p.id) || [],
2617
- organizationPlugin: hasOrganizationPlugin || null,
2618
- });
2619
- }
2620
- res.json({
2621
- enabled: false,
2622
- error: 'Failed to load auth config - getConfig failed and regex extraction unavailable',
2623
- configPath: authConfigPath,
2624
- });
2625
- }
2626
- catch (_error) {
2627
- res.status(500).json({ error: 'Failed to check plugin status' });
2628
- }
2985
+ const plugins = betterAuthConfig?.plugins || [];
2986
+ const hasOrganizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
2987
+ return res.json({
2988
+ enabled: !!hasOrganizationPlugin,
2989
+ configPath: configPath || null,
2990
+ availablePlugins: plugins.map((p) => p.id) || [],
2991
+ organizationPlugin: hasOrganizationPlugin || null,
2992
+ });
2629
2993
  }
2630
2994
  catch (_error) {
2631
2995
  res.status(500).json({ error: 'Failed to check plugin status' });
@@ -2854,7 +3218,6 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
2854
3218
  if (!adapter) {
2855
3219
  return res.status(500).json({ error: 'Auth adapter not available' });
2856
3220
  }
2857
- // @ts-expect-error
2858
3221
  const user = await adapter.findOne({
2859
3222
  model: 'user',
2860
3223
  where: [{ field: 'id', value: userId }],
@@ -2908,7 +3271,6 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
2908
3271
  if (!adapter) {
2909
3272
  return res.status(500).json({ error: 'Auth adapter not available' });
2910
3273
  }
2911
- // @ts-expect-error
2912
3274
  const user = await adapter.findOne({
2913
3275
  model: 'user',
2914
3276
  where: [{ field: 'id', value: userId }],
@@ -3109,7 +3471,17 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
3109
3471
  });
3110
3472
  router.get('/api/tools/oauth/providers', async (_req, res) => {
3111
3473
  try {
3112
- const providers = authConfig.socialProviders || [];
3474
+ const effectiveConfig = preloadedAuthOptions || authConfig || {};
3475
+ const socialProviders = effectiveConfig.socialProviders || {};
3476
+ const providers = Array.isArray(socialProviders)
3477
+ ? socialProviders
3478
+ : Object.entries(socialProviders).map(([id, provider]) => ({
3479
+ id,
3480
+ name: provider.name || id,
3481
+ type: id,
3482
+ enabled: !!(provider.clientId && provider.clientSecret),
3483
+ ...provider,
3484
+ }));
3113
3485
  res.json({
3114
3486
  success: true,
3115
3487
  providers: providers.map((provider) => ({
@@ -3120,7 +3492,8 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
3120
3492
  })),
3121
3493
  });
3122
3494
  }
3123
- catch (_error) {
3495
+ catch (error) {
3496
+ console.error('Failed to fetch OAuth providers:', error);
3124
3497
  res.status(500).json({ success: false, error: 'Failed to fetch OAuth providers' });
3125
3498
  }
3126
3499
  });
@@ -3130,7 +3503,15 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
3130
3503
  if (!provider) {
3131
3504
  return res.status(400).json({ success: false, error: 'Provider is required' });
3132
3505
  }
3133
- const providers = authConfig.socialProviders || [];
3506
+ const effectiveConfig = preloadedAuthOptions || authConfig || {};
3507
+ const socialProviders = effectiveConfig.socialProviders || {};
3508
+ const providers = Array.isArray(socialProviders)
3509
+ ? socialProviders
3510
+ : Object.entries(socialProviders).map(([id, p]) => ({
3511
+ id,
3512
+ type: id,
3513
+ ...p,
3514
+ }));
3134
3515
  const selectedProvider = providers.find((p) => (p.id || p.type) === provider);
3135
3516
  if (!selectedProvider) {
3136
3517
  return res.status(404).json({ success: false, error: 'Provider not found' });
@@ -5004,4 +5385,126 @@ export const authClient = createAuthClient({
5004
5385
  });
5005
5386
  return router;
5006
5387
  }
5388
+ export async function handleStudioApiRequest(ctx) {
5389
+ let preloadedAdapter = null;
5390
+ if (ctx.auth) {
5391
+ try {
5392
+ const context = await ctx.auth.$context;
5393
+ if (context?.adapter) {
5394
+ preloadedAdapter = context.adapter;
5395
+ }
5396
+ }
5397
+ catch { }
5398
+ }
5399
+ const authOptions = ctx.auth?.options || null;
5400
+ const router = createRoutes(ctx.auth, ctx.configPath || '', undefined, preloadedAdapter, authOptions, ctx.accessConfig, ctx.auth);
5401
+ const [pathname, queryString] = ctx.path.split('?');
5402
+ const query = {};
5403
+ if (queryString) {
5404
+ queryString.split('&').forEach((param) => {
5405
+ const [key, value] = param.split('=');
5406
+ if (key)
5407
+ query[key] = decodeURIComponent(value || '');
5408
+ });
5409
+ }
5410
+ try {
5411
+ const route = findMatchingRoute(router, pathname, ctx.method);
5412
+ if (!route) {
5413
+ return { status: 404, data: { error: 'Not found', path: pathname } };
5414
+ }
5415
+ const cookies = [];
5416
+ const parseCookies = (cookieHeader) => {
5417
+ const result = {};
5418
+ if (cookieHeader) {
5419
+ cookieHeader.split(';').forEach((cookie) => {
5420
+ const [key, ...rest] = cookie.split('=');
5421
+ if (key)
5422
+ result[key.trim()] = rest.join('=').trim();
5423
+ });
5424
+ }
5425
+ return result;
5426
+ };
5427
+ const mockReq = {
5428
+ method: ctx.method,
5429
+ url: ctx.path,
5430
+ path: pathname,
5431
+ originalUrl: ctx.path,
5432
+ headers: ctx.headers,
5433
+ body: ctx.body,
5434
+ query: query,
5435
+ params: route.params,
5436
+ cookies: parseCookies(ctx.headers['cookie'] || ctx.headers['Cookie'] || ''),
5437
+ };
5438
+ let responseStatus = 200;
5439
+ let responseData = {};
5440
+ const mockRes = {
5441
+ status: (code) => {
5442
+ responseStatus = code;
5443
+ return mockRes;
5444
+ },
5445
+ json: (data) => {
5446
+ responseData = data;
5447
+ return mockRes;
5448
+ },
5449
+ send: (data) => {
5450
+ responseData = data;
5451
+ return mockRes;
5452
+ },
5453
+ cookie: (name, value, options) => {
5454
+ cookies.push({ name, value, options });
5455
+ return mockRes;
5456
+ },
5457
+ redirect: (url) => {
5458
+ responseStatus = 302;
5459
+ responseData = { redirect: url };
5460
+ return mockRes;
5461
+ },
5462
+ };
5463
+ await route.handler(mockReq, mockRes);
5464
+ return { status: responseStatus, data: responseData, cookies };
5465
+ }
5466
+ catch (error) {
5467
+ console.error('Studio API error:', error);
5468
+ return { status: 500, data: { error: 'Internal server error' } };
5469
+ }
5470
+ }
5471
+ function findMatchingRoute(router, path, method) {
5472
+ const routes = router.stack || [];
5473
+ for (const layer of routes) {
5474
+ if (layer.route) {
5475
+ const routePath = layer.route.path;
5476
+ const routeMethods = Object.keys(layer.route.methods);
5477
+ if (routeMethods.includes(method.toLowerCase())) {
5478
+ const params = extractParams(routePath, path);
5479
+ if (params !== null) {
5480
+ return {
5481
+ handler: layer.route.stack[0].handle,
5482
+ params,
5483
+ };
5484
+ }
5485
+ }
5486
+ }
5487
+ }
5488
+ return null;
5489
+ }
5490
+ function extractParams(routePath, requestPath) {
5491
+ if (routePath === requestPath)
5492
+ return {};
5493
+ const paramNames = [];
5494
+ const routeRegex = routePath
5495
+ .replace(/:([^/]+)/g, (_, paramName) => {
5496
+ paramNames.push(paramName);
5497
+ return '([^/]+)';
5498
+ })
5499
+ .replace(/\*/g, '.*');
5500
+ const regex = new RegExp(`^${routeRegex}$`);
5501
+ const match = requestPath.match(regex);
5502
+ if (!match)
5503
+ return null;
5504
+ const params = {};
5505
+ paramNames.forEach((name, index) => {
5506
+ params[name] = match[index + 1];
5507
+ });
5508
+ return params;
5509
+ }
5007
5510
  //# sourceMappingURL=routes.js.map