better-auth-studio 1.0.27 → 1.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/routes.js CHANGED
@@ -1,12 +1,30 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ // @ts-expect-error
5
+ import { hex } from '@better-auth/utils/hex';
6
+ import { scryptAsync } from '@noble/hashes/scrypt.js';
4
7
  import { Router } from 'express';
5
8
  import { createJiti } from 'jiti';
6
9
  import { createMockAccount, createMockSession, createMockUser, createMockVerification, getAuthAdapter, } from './auth-adapter.js';
7
10
  import { getAuthData } from './data.js';
8
11
  import { initializeGeoService, resolveIPLocation, setGeoDbPath } from './geo-service.js';
9
12
  import { detectDatabaseWithDialect } from './utils/database-detection.js';
13
+ const config = {
14
+ N: 16384,
15
+ r: 16,
16
+ p: 1,
17
+ dkLen: 64,
18
+ };
19
+ async function generateKey(password, salt) {
20
+ return await scryptAsync(password.normalize('NFKC'), salt, {
21
+ N: config.N,
22
+ p: config.p,
23
+ r: config.r,
24
+ dkLen: config.dkLen,
25
+ maxmem: 128 * config.N * config.r * 2,
26
+ });
27
+ }
10
28
  function getStudioVersion() {
11
29
  try {
12
30
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -212,7 +230,61 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
212
230
  },
213
231
  });
214
232
  });
215
- // IP Geolocation endpoint
233
+ router.get('/api/version-check', async (_req, res) => {
234
+ try {
235
+ const __dirname = dirname(fileURLToPath(import.meta.url));
236
+ const projectRoot = join(__dirname, '..');
237
+ let currentVersion = '1.0.0';
238
+ try {
239
+ const betterAuthPkgPath = join(projectRoot, 'node_modules', 'better-auth', 'package.json');
240
+ if (existsSync(betterAuthPkgPath)) {
241
+ const betterAuthPkg = JSON.parse(readFileSync(betterAuthPkgPath, 'utf-8'));
242
+ currentVersion = betterAuthPkg.version || '1.0.0';
243
+ }
244
+ }
245
+ catch (_error) {
246
+ try {
247
+ const packageJsonPath = join(projectRoot, 'package.json');
248
+ if (existsSync(packageJsonPath)) {
249
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
250
+ const versionString = packageJson.dependencies?.['better-auth'] ||
251
+ packageJson.devDependencies?.['better-auth'] ||
252
+ '1.0.0';
253
+ currentVersion = versionString.replace(/[\^~>=<]/g, '');
254
+ }
255
+ }
256
+ catch { }
257
+ }
258
+ let latestVersion = currentVersion;
259
+ let isOutdated = false;
260
+ try {
261
+ const npmResponse = await fetch('https://registry.npmjs.org/better-auth/latest');
262
+ if (npmResponse.ok) {
263
+ const npmData = await npmResponse.json();
264
+ latestVersion = npmData.version || currentVersion;
265
+ isOutdated = currentVersion !== latestVersion;
266
+ }
267
+ }
268
+ catch (_fetchError) {
269
+ latestVersion = currentVersion;
270
+ isOutdated = false;
271
+ }
272
+ res.json({
273
+ current: currentVersion,
274
+ latest: latestVersion,
275
+ isOutdated,
276
+ updateCommand: 'npm install better-auth@latest',
277
+ });
278
+ }
279
+ catch (_error) {
280
+ res.status(500).json({
281
+ error: 'Failed to check version',
282
+ current: 'unknown',
283
+ latest: 'unknown',
284
+ isOutdated: false,
285
+ });
286
+ }
287
+ });
216
288
  router.post('/api/geo/resolve', (req, res) => {
217
289
  try {
218
290
  const { ipAddress } = req.body;
@@ -246,6 +318,14 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
246
318
  let databaseDialect = 'unknown';
247
319
  let databaseAdapter = 'unknown';
248
320
  let databaseVersion = 'unknown';
321
+ let adapterConfig = null;
322
+ try {
323
+ const adapterResult = await getAuthAdapterWithConfig();
324
+ if (adapterResult && adapterResult.options?.adapterConfig) {
325
+ adapterConfig = adapterResult.options.adapterConfig;
326
+ }
327
+ }
328
+ catch (_error) { }
249
329
  try {
250
330
  const detectedDb = await detectDatabaseWithDialect();
251
331
  if (detectedDb) {
@@ -291,6 +371,7 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
291
371
  casing: authConfig.database?.casing || 'camel',
292
372
  debugLogs: authConfig.database?.debugLogs || false,
293
373
  url: authConfig.database?.url,
374
+ adapterConfig: adapterConfig,
294
375
  },
295
376
  emailVerification: {
296
377
  sendOnSignUp: authConfig.emailVerification?.sendOnSignUp || false,
@@ -363,6 +444,8 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
363
444
  max: authConfig.rateLimit?.max || 100,
364
445
  storage: authConfig.rateLimit?.storage || 'memory',
365
446
  modelName: authConfig.rateLimit?.modelName || 'rateLimit',
447
+ customStorage: authConfig.rateLimit?.customStorage || null,
448
+ customRules: authConfig.rateLimit?.customRules || [],
366
449
  },
367
450
  advanced: {
368
451
  ipAddress: {
@@ -407,6 +490,21 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
407
490
  res.status(500).json({ error: 'Failed to fetch statistics' });
408
491
  }
409
492
  });
493
+ router.get('/api/analytics', async (req, res) => {
494
+ try {
495
+ const { period = 'ALL', type = 'users', from, to } = req.query;
496
+ const analytics = await getAuthData(authConfig, 'analytics', {
497
+ period: period,
498
+ type: type,
499
+ from: from,
500
+ to: to,
501
+ }, configPath);
502
+ res.json(analytics);
503
+ }
504
+ catch (_error) {
505
+ res.status(500).json({ error: 'Failed to fetch analytics' });
506
+ }
507
+ });
410
508
  router.get('/api/counts', async (_req, res) => {
411
509
  try {
412
510
  const adapter = await getAuthAdapterWithConfig();
@@ -429,6 +527,7 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
429
527
  const plugins = betterAuthConfig.plugins || [];
430
528
  const organizationPlugin = plugins.find((plugin) => plugin.id === 'organization');
431
529
  organizationPluginEnabled = !!organizationPlugin;
530
+ teamsPluginEnabled = !!organizationPlugin?.options?.teams?.enabled;
432
531
  if (organizationPlugin) {
433
532
  teamsPluginEnabled = organizationPlugin.options?.teams?.enabled === true;
434
533
  }
@@ -442,14 +541,14 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
442
541
  if (adapter) {
443
542
  try {
444
543
  if (typeof adapter.findMany === 'function') {
445
- const users = await adapter.findMany({ model: 'user', limit: 10000 });
544
+ const users = await adapter.findMany({ model: 'user', limit: 100000 });
446
545
  userCount = users?.length || 0;
447
546
  }
448
547
  }
449
548
  catch (_error) { }
450
549
  try {
451
550
  if (typeof adapter.findMany === 'function') {
452
- const sessions = await adapter.findMany({ model: 'session', limit: 10000 });
551
+ const sessions = await adapter.findMany({ model: 'session', limit: 100000 });
453
552
  sessionCount = sessions?.length || 0;
454
553
  }
455
554
  }
@@ -494,13 +593,15 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
494
593
  if (!adapter) {
495
594
  return res.status(500).json({ error: 'Auth adapter not available' });
496
595
  }
497
- if (adapter.getUsers) {
498
- const users = await adapter.getUsers();
499
- res.json({ success: true, users });
596
+ let users = [];
597
+ if (adapter.findMany) {
598
+ // Use findMany with high limit to get all users
599
+ users = await adapter.findMany({ model: 'user', limit: 100000 }).catch(() => []);
500
600
  }
501
- else {
502
- res.json({ success: true, users: [] });
601
+ else if (adapter.getUsers) {
602
+ users = await adapter.getUsers();
503
603
  }
604
+ res.json({ success: true, users });
504
605
  }
505
606
  catch (_error) {
506
607
  res.status(500).json({ error: 'Failed to fetch users' });
@@ -547,6 +648,40 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
547
648
  res.status(500).json({ error: 'Failed to update user' });
548
649
  }
549
650
  });
651
+ router.put('/api/users/:userId/password', async (req, res) => {
652
+ try {
653
+ const { userId } = req.params;
654
+ const { password } = req.body;
655
+ if (!password) {
656
+ return res.status(400).json({ error: 'Password is required' });
657
+ }
658
+ const adapter = await getAuthAdapterWithConfig();
659
+ if (!adapter || !adapter.update) {
660
+ return res.status(500).json({ error: 'Auth adapter not available' });
661
+ }
662
+ let hashedPassword = password;
663
+ try {
664
+ const salt = hex.encode(crypto.getRandomValues(new Uint8Array(16)));
665
+ const key = await generateKey(password, salt);
666
+ hashedPassword = `${salt}:${hex.encode(key)}`;
667
+ }
668
+ catch {
669
+ res.status(500).json({ error: 'Failed to hash password' });
670
+ }
671
+ const account = await adapter.update({
672
+ model: 'account',
673
+ where: [
674
+ { field: 'userId', value: userId },
675
+ { field: 'providerId', value: 'credential' },
676
+ ],
677
+ update: { password: hashedPassword },
678
+ });
679
+ res.json({ success: true, account });
680
+ }
681
+ catch (error) {
682
+ res.status(500).json({ error: 'Failed to update password', message: error?.message });
683
+ }
684
+ });
550
685
  router.delete('/api/users/:userId', async (req, res) => {
551
686
  try {
552
687
  const { userId } = req.params;
@@ -629,12 +764,14 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
629
764
  organizationName: organization
630
765
  ? organization.name || 'Unknown Organization'
631
766
  : 'Unknown Organization',
767
+ organizationSlug: organization ? organization.slug || 'unknown' : 'unknown',
632
768
  }
633
769
  : {
634
770
  id: membership.teamId,
635
771
  name: 'Unknown Team',
636
772
  organizationId: 'unknown',
637
773
  organizationName: 'Unknown Organization',
774
+ organizationSlug: 'unknown',
638
775
  },
639
776
  role: membership.role || 'member',
640
777
  joinedAt: membership.createdAt,
@@ -728,7 +865,7 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
728
865
  if (!adapter || !adapter.delete) {
729
866
  return res.status(500).json({ error: 'Auth adapter not available' });
730
867
  }
731
- await adapter.delete({ model: 'session', id: sessionId });
868
+ await adapter.delete({ model: 'session', where: [{ field: 'id', value: sessionId }] });
732
869
  res.json({ success: true });
733
870
  }
734
871
  catch (_error) {
@@ -821,12 +958,11 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
821
958
  try {
822
959
  const adapter = await getAuthAdapterWithConfig();
823
960
  if (adapter && typeof adapter.findMany === 'function') {
824
- // If limit is very high (like 10000), fetch all users without pagination
825
961
  const shouldPaginate = limit < 1000;
826
962
  const fetchLimit = shouldPaginate ? limit : undefined;
827
963
  const allUsers = await adapter.findMany({
828
964
  model: 'user',
829
- limit: fetchLimit
965
+ limit: fetchLimit,
830
966
  });
831
967
  let filteredUsers = allUsers || [];
832
968
  if (search) {
@@ -1048,6 +1184,51 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1048
1184
  res.status(500).json({ error: 'Failed to fetch database info' });
1049
1185
  }
1050
1186
  });
1187
+ router.get('/api/database/test', async (_req, res) => {
1188
+ try {
1189
+ const adapter = await getAuthAdapterWithConfig();
1190
+ if (!adapter || !adapter.findMany) {
1191
+ return res.status(500).json({ error: 'Auth adapter not available' });
1192
+ }
1193
+ const result = await adapter.findMany({
1194
+ model: 'user',
1195
+ limit: 1,
1196
+ });
1197
+ return res.json({ success: true, result: result });
1198
+ }
1199
+ catch (error) {
1200
+ res.status(500).json({
1201
+ success: false,
1202
+ error: 'Failed to test database connection',
1203
+ message: error instanceof Error ? error.message : 'Unknown error',
1204
+ });
1205
+ }
1206
+ });
1207
+ router.post('/api/tools/migrations/run', async (req, res) => {
1208
+ try {
1209
+ const { provider, script } = req.body;
1210
+ if (!provider) {
1211
+ return res.status(400).json({ success: false, error: 'Migration provider is required' });
1212
+ }
1213
+ if (script) {
1214
+ }
1215
+ else {
1216
+ }
1217
+ // This endpoint does not execute arbitrary scripts for safety. It simply
1218
+ // acknowledges receipt so the frontend can present instructions.
1219
+ return res.json({
1220
+ success: true,
1221
+ message: 'Migration script received. Review the server logs for details.',
1222
+ });
1223
+ }
1224
+ catch (error) {
1225
+ res.status(500).json({
1226
+ success: false,
1227
+ error: 'Failed to process migration request',
1228
+ message: error instanceof Error ? error.message : 'Unknown error',
1229
+ });
1230
+ }
1231
+ });
1051
1232
  // Database Detection endpoint - Auto-detect database from installed packages
1052
1233
  router.get('/api/database/detect', async (_req, res) => {
1053
1234
  try {
@@ -1246,9 +1427,6 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
1246
1427
  enabled: !!adminPlugin,
1247
1428
  configPath: authConfigPath,
1248
1429
  adminPlugin: adminPlugin || null,
1249
- message: adminPlugin
1250
- ? 'Admin plugin is enabled. Use Better Auth admin endpoints directly for ban/unban functionality.'
1251
- : 'Admin plugin is not enabled. Please enable the admin plugin in your Better Auth configuration.',
1252
1430
  });
1253
1431
  }
1254
1432
  catch (error) {
@@ -2597,15 +2775,15 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
2597
2775
  });
2598
2776
  router.get('/api/organizations', async (req, res) => {
2599
2777
  try {
2600
- const page = parseInt(req.query.page, 10) || 1;
2778
+ const _page = parseInt(req.query.page, 10) || 1;
2601
2779
  const limit = parseInt(req.query.limit, 10) || 20;
2602
- const search = req.query.search;
2780
+ const _search = req.query.search;
2603
2781
  try {
2604
2782
  const adapter = await getAuthAdapterWithConfig();
2605
2783
  if (adapter && typeof adapter.findMany === 'function') {
2606
2784
  const allOrganizations = await adapter.findMany({
2607
2785
  model: 'organization',
2608
- limit: limit
2786
+ limit: limit,
2609
2787
  });
2610
2788
  res.json({ organizations: allOrganizations });
2611
2789
  }
@@ -3004,6 +3182,444 @@ export function createRoutes(authConfig, configPath, geoDbPath) {
3004
3182
  res.status(500).json({ error: 'Failed to seed organizations' });
3005
3183
  }
3006
3184
  });
3185
+ // OAuth Test Endpoints
3186
+ router.get('/api/tools/oauth/providers', async (_req, res) => {
3187
+ const _result = await getAuthAdapterWithConfig();
3188
+ try {
3189
+ const providers = authConfig.socialProviders || [];
3190
+ res.json({
3191
+ success: true,
3192
+ providers: providers.map((provider) => ({
3193
+ id: provider.id || provider.type,
3194
+ name: provider.name || provider.id || provider.type,
3195
+ type: provider.type || provider.id,
3196
+ enabled: provider.enabled !== false,
3197
+ })),
3198
+ });
3199
+ }
3200
+ catch (_error) {
3201
+ res.status(500).json({ success: false, error: 'Failed to fetch OAuth providers' });
3202
+ }
3203
+ });
3204
+ router.post('/api/tools/oauth/test', async (req, res) => {
3205
+ try {
3206
+ const { provider } = req.body;
3207
+ if (!provider) {
3208
+ return res.status(400).json({ success: false, error: 'Provider is required' });
3209
+ }
3210
+ // Check if provider exists
3211
+ const providers = authConfig.socialProviders || [];
3212
+ const selectedProvider = providers.find((p) => (p.id || p.type) === provider);
3213
+ if (!selectedProvider) {
3214
+ return res.status(404).json({ success: false, error: 'Provider not found' });
3215
+ }
3216
+ // Generate test session ID
3217
+ const testSessionId = `oauth-test-${Date.now()}-${Math.random().toString(36).substring(7)}`;
3218
+ // Store the test session
3219
+ oauthTestSessions.set(testSessionId, {
3220
+ provider,
3221
+ startTime: Date.now(),
3222
+ status: 'pending',
3223
+ });
3224
+ const studioBaseUrl = `${req.protocol}://${req.get('host')}`;
3225
+ res.json({
3226
+ success: true,
3227
+ startUrl: `${studioBaseUrl}/api/tools/oauth/start?testSessionId=${encodeURIComponent(testSessionId)}&provider=${encodeURIComponent(provider)}`,
3228
+ testSessionId,
3229
+ provider: selectedProvider.name || selectedProvider.id || selectedProvider.type,
3230
+ });
3231
+ }
3232
+ catch (error) {
3233
+ res.status(500).json({
3234
+ success: false,
3235
+ error: 'Failed to initiate OAuth test',
3236
+ details: error instanceof Error ? error.message : String(error),
3237
+ });
3238
+ }
3239
+ });
3240
+ // Store OAuth test sessions and results temporarily
3241
+ const oauthTestSessions = new Map();
3242
+ const oauthTestResults = new Map();
3243
+ router.get('/api/tools/oauth/start', async (req, res) => {
3244
+ try {
3245
+ const { testSessionId, provider } = req.query;
3246
+ if (!testSessionId || !provider) {
3247
+ return res
3248
+ .status(400)
3249
+ .send('<html><body style="background:#000;color:#fff;font-family:monospace;padding:20px;">Missing test session or provider</body></html>');
3250
+ }
3251
+ const session = oauthTestSessions.get(testSessionId);
3252
+ if (!session || session.provider !== provider) {
3253
+ return res
3254
+ .status(404)
3255
+ .send('<html><body style="background:#000;color:#fff;font-family:monospace;padding:20px;">OAuth test session not found</body></html>');
3256
+ }
3257
+ const authBaseUrl = authConfig.baseURL || 'http://localhost:3000';
3258
+ const basePath = authConfig.basePath || '/api/auth';
3259
+ const payload = {
3260
+ provider,
3261
+ additionalData: { testSessionId },
3262
+ };
3263
+ res.setHeader('Content-Type', 'text/html');
3264
+ res.send(`
3265
+ <!DOCTYPE html>
3266
+ <html>
3267
+ <head>
3268
+ <title>Starting OAuth Test</title>
3269
+ <link rel="preconnect" href="https://fonts.googleapis.com">
3270
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
3271
+ <link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
3272
+ <style>
3273
+ :root { color-scheme: dark; }
3274
+ body {
3275
+ background: #0b0b0f;
3276
+ color: #fff;
3277
+ font-family: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3278
+ display: flex;
3279
+ align-items: center;
3280
+ justify-content: center;
3281
+ min-height: 100vh;
3282
+ margin: 0;
3283
+ }
3284
+ .box {
3285
+ text-align: center;
3286
+ max-width: 520px;
3287
+ font-family: "Geist Mono", monospace;
3288
+ }
3289
+ h1 {
3290
+ font-family: "Geist", sans-serif;
3291
+ letter-spacing: 0.08em;
3292
+ text-transform: uppercase;
3293
+ font-weight: 500;
3294
+ }
3295
+ p {
3296
+ font-family: "Geist Mono", monospace;
3297
+ font-size: 13px;
3298
+ color: #9ca3af;
3299
+ }
3300
+ .spinner {
3301
+ border: 3px solid rgba(255,255,255,0.12);
3302
+ border-top: 3px solid #fff;
3303
+ border-radius: 50%;
3304
+ width: 40px;
3305
+ height: 40px;
3306
+ animation: spin 1s linear infinite;
3307
+ margin: 24px auto;
3308
+ }
3309
+ @keyframes spin { 0% { transform: rotate(0deg);} 100% { transform: rotate(360deg);} }
3310
+ button {
3311
+ background: #111118;
3312
+ border: 1px solid #27272a;
3313
+ color: #fff;
3314
+ padding: 10px 16px;
3315
+ border-radius: 6px;
3316
+ cursor: pointer;
3317
+ font-family: "Geist", sans-serif;
3318
+ text-transform: uppercase;
3319
+ letter-spacing: 0.08em;
3320
+ font-size: 12px;
3321
+ }
3322
+ button:hover { background: #1d1d26; }
3323
+ </style>
3324
+ </head>
3325
+ <body>
3326
+ <div class="box">
3327
+ <h1>Preparing OAuth Test…</h1>
3328
+ <p id="status">Contacting Better Auth to generate a secure state.</p>
3329
+ <div class="spinner" id="spinner"></div>
3330
+ <button id="retry" style="display:none;">Retry</button>
3331
+ </div>
3332
+ <script>
3333
+ const payload = ${JSON.stringify(payload)};
3334
+ const endpoint = ${JSON.stringify(`${authBaseUrl}${basePath}/sign-in/social`)};
3335
+ const statusEl = document.getElementById('status');
3336
+ const retryBtn = document.getElementById('retry');
3337
+ const spinner = document.getElementById('spinner');
3338
+
3339
+ const postToParent = (data) => {
3340
+ if (window.opener) {
3341
+ try {
3342
+ window.opener.postMessage({
3343
+ type: 'oauth_test_state',
3344
+ ...data,
3345
+ }, window.location.origin);
3346
+ } catch (err) {
3347
+ console.error('postMessage failed', err);
3348
+ }
3349
+ }
3350
+ };
3351
+
3352
+ async function startOAuth() {
3353
+ try {
3354
+ const response = await fetch(endpoint, {
3355
+ method: 'POST',
3356
+ credentials: 'include',
3357
+ headers: {
3358
+ 'Content-Type': 'application/json'
3359
+ },
3360
+ body: JSON.stringify(payload)
3361
+ });
3362
+
3363
+ if (response.redirected) {
3364
+ postToParent({ status: 'redirect', testSessionId: payload.additionalData?.testSessionId });
3365
+ window.location.href = response.url;
3366
+ return;
3367
+ }
3368
+
3369
+ let data = null;
3370
+ const contentType = response.headers.get('content-type') || '';
3371
+ if (contentType.includes('application/json')) {
3372
+ data = await response.json().catch(() => null);
3373
+ }
3374
+
3375
+ const redirectUrl = data?.url || data?.redirect || data?.location;
3376
+ if (redirectUrl) {
3377
+ postToParent({ status: 'redirect', testSessionId: payload.additionalData?.testSessionId });
3378
+ window.location.href = redirectUrl;
3379
+ return;
3380
+ }
3381
+
3382
+ if (response.status >= 400) {
3383
+ throw new Error(data?.message || 'Better Auth returned an error');
3384
+ }
3385
+
3386
+ throw new Error('Unable to determine OAuth redirect URL.');
3387
+ } catch (error) {
3388
+ console.error('Failed to start OAuth test', error);
3389
+ statusEl.textContent = 'Failed to start OAuth test: ' + (error?.message || error);
3390
+ spinner.style.display = 'none';
3391
+ retryBtn.style.display = 'inline-flex';
3392
+ postToParent({
3393
+ status: 'error',
3394
+ testSessionId: payload.additionalData?.testSessionId,
3395
+ error: error?.message || String(error),
3396
+ });
3397
+ }
3398
+ }
3399
+
3400
+ retryBtn.addEventListener('click', () => {
3401
+ spinner.style.display = 'block';
3402
+ retryBtn.style.display = 'none';
3403
+ statusEl.textContent = 'Retrying…';
3404
+ startOAuth();
3405
+ });
3406
+
3407
+ startOAuth();
3408
+ </script>
3409
+ </body>
3410
+ </html>
3411
+ `);
3412
+ }
3413
+ catch (_error) {
3414
+ res
3415
+ .status(500)
3416
+ .send('<html><body style="background:#000;color:#fff;font-family:monospace;padding:20px;">Failed to start OAuth test</body></html>');
3417
+ }
3418
+ });
3419
+ router.get('/api/tools/oauth/callback', async (req, res) => {
3420
+ try {
3421
+ const { testSessionId, error: oauthError } = req.query;
3422
+ if (!testSessionId) {
3423
+ return res.send(`<html><body style="background:#000;color:#fff;text-align:center;">
3424
+ <h1>OAuth Test Failed</h1>
3425
+ <p>Missing test session</p>
3426
+ <script>setTimeout(() => window.close(), 3000);</script>
3427
+ </body></html>`);
3428
+ }
3429
+ const testSession = oauthTestSessions.get(testSessionId);
3430
+ if (!testSession) {
3431
+ return res.send(`<html><body style="background:#000;color:#fff;text-align:center;">
3432
+ <h1>OAuth Test Failed</h1>
3433
+ <p>Test session not found or expired</p>
3434
+ <script>setTimeout(() => window.close(), 3000);</script>
3435
+ </body></html>`);
3436
+ }
3437
+ const result = {
3438
+ testSessionId: testSessionId,
3439
+ provider: testSession.provider,
3440
+ success: !oauthError,
3441
+ error: oauthError,
3442
+ timestamp: new Date().toISOString(),
3443
+ };
3444
+ oauthTestResults.set(testSessionId, result);
3445
+ res.send(`
3446
+ <!DOCTYPE html>
3447
+ <html>
3448
+ <head>
3449
+ <title>OAuth Test ${oauthError ? 'Failed' : 'Success'}</title>
3450
+ <link rel="preconnect" href="https://fonts.googleapis.com">
3451
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
3452
+ <link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
3453
+ <style>
3454
+ body {
3455
+ font-family: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3456
+ background: #000;
3457
+ color: #fff;
3458
+ display: flex;
3459
+ align-items: center;
3460
+ justify-content: center;
3461
+ min-height: 100vh;
3462
+ text-align: center;
3463
+ margin: 0;
3464
+ padding: 24px;
3465
+ }
3466
+ h1 {
3467
+ font-weight: 500;
3468
+ letter-spacing: 0.1em;
3469
+ text-transform: uppercase;
3470
+ }
3471
+ p {
3472
+ font-family: "Geist Mono", monospace;
3473
+ font-size: 13px;
3474
+ color: #9ca3af;
3475
+ }
3476
+ .success { color: #4f4; }
3477
+ .error { color: #f44; }
3478
+ .box {
3479
+ max-width: 520px;
3480
+ }
3481
+ </style>
3482
+ </head>
3483
+ <body>
3484
+ <div class="box">
3485
+ <h1 class="${oauthError ? 'error' : 'success'}">
3486
+ ${oauthError ? '❌ OAuth Test Failed' : '✅ OAuth Test Completed'}
3487
+ </h1>
3488
+ <p>${oauthError ? oauthError : 'Waiting for account creation...'}</p>
3489
+ </div>
3490
+ <script>
3491
+ if (window.opener) {
3492
+ window.opener.postMessage({
3493
+ type: 'oauth_test_result',
3494
+ result: ${JSON.stringify(result)}
3495
+ }, '*');
3496
+ setTimeout(() => window.close(), 1500);
3497
+ }
3498
+ </script>
3499
+ </body>
3500
+ </html>
3501
+ `);
3502
+ }
3503
+ catch (_error) {
3504
+ res.send('<html><body style="background:#000;color:#fff;text-align:center;"><h1>OAuth Test Error</h1><p>Callback processing failed</p></body></html>');
3505
+ }
3506
+ });
3507
+ router.get('/api/tools/oauth/status', async (req, res) => {
3508
+ try {
3509
+ const { testSessionId } = req.query;
3510
+ if (!testSessionId) {
3511
+ return res.json({ hasResult: false });
3512
+ }
3513
+ const cached = oauthTestResults.get(testSessionId);
3514
+ if (cached) {
3515
+ oauthTestResults.delete(testSessionId);
3516
+ return res.json({ hasResult: true, result: cached });
3517
+ }
3518
+ const session = oauthTestSessions.get(testSessionId);
3519
+ if (!session) {
3520
+ return res.json({ hasResult: false });
3521
+ }
3522
+ const adapter = await getAuthAdapterWithConfig();
3523
+ if (!adapter || !adapter.findMany) {
3524
+ return res.json({ hasResult: false });
3525
+ }
3526
+ const startTime = session.startTime;
3527
+ const provider = session.provider;
3528
+ const parseDate = (value) => {
3529
+ if (!value)
3530
+ return 0;
3531
+ const date = value instanceof Date ? value : new Date(value);
3532
+ return date.getTime();
3533
+ };
3534
+ const bufferMs = 5000;
3535
+ const threshold = startTime - bufferMs;
3536
+ let recentAccount = null;
3537
+ let recentSession = null;
3538
+ try {
3539
+ const accounts = await adapter.findMany({
3540
+ model: 'account',
3541
+ where: [{ field: 'providerId', value: provider }],
3542
+ limit: 50,
3543
+ });
3544
+ const accountCandidate = accounts
3545
+ .map((account) => ({
3546
+ account,
3547
+ created: parseDate(account.createdAt || account.created_at || account.updatedAt || account.updated_at),
3548
+ }))
3549
+ .filter((entry) => entry.created >= threshold)
3550
+ .sort((a, b) => b.created - a.created)[0];
3551
+ recentAccount = accountCandidate?.account ?? null;
3552
+ }
3553
+ catch (_accountError) { }
3554
+ try {
3555
+ const sessions = await adapter.findMany({
3556
+ model: 'session',
3557
+ limit: 50,
3558
+ });
3559
+ const sessionCandidate = sessions
3560
+ .map((sessionItem) => ({
3561
+ session: sessionItem,
3562
+ created: parseDate(sessionItem.createdAt ||
3563
+ sessionItem.created_at ||
3564
+ sessionItem.updatedAt ||
3565
+ sessionItem.updated_at),
3566
+ }))
3567
+ .filter((entry) => entry.created >= threshold)
3568
+ .sort((a, b) => b.created - a.created)[0];
3569
+ recentSession = sessionCandidate?.session ?? null;
3570
+ }
3571
+ catch (_sessionError) { }
3572
+ if (recentAccount || recentSession) {
3573
+ let userInfo = null;
3574
+ try {
3575
+ const userId = recentAccount?.userId || recentSession?.userId;
3576
+ if (userId) {
3577
+ const users = await adapter.findMany({
3578
+ model: 'user',
3579
+ where: [{ field: 'id', value: userId }],
3580
+ limit: 1,
3581
+ });
3582
+ if (users && users.length > 0) {
3583
+ const user = users[0];
3584
+ userInfo = {
3585
+ id: user.id,
3586
+ name: user.name,
3587
+ email: user.email,
3588
+ image: user.image,
3589
+ };
3590
+ }
3591
+ }
3592
+ }
3593
+ catch (_userError) { }
3594
+ const result = {
3595
+ testSessionId: testSessionId,
3596
+ provider,
3597
+ success: true,
3598
+ userInfo,
3599
+ account: recentAccount
3600
+ ? {
3601
+ id: recentAccount.id,
3602
+ userId: recentAccount.userId,
3603
+ }
3604
+ : null,
3605
+ session: recentSession
3606
+ ? {
3607
+ id: recentSession.id,
3608
+ userId: recentSession.userId,
3609
+ }
3610
+ : null,
3611
+ timestamp: new Date().toISOString(),
3612
+ };
3613
+ oauthTestResults.set(testSessionId, result);
3614
+ oauthTestSessions.delete(testSessionId);
3615
+ return res.json({ hasResult: true, result });
3616
+ }
3617
+ res.json({ hasResult: false });
3618
+ }
3619
+ catch (_error) {
3620
+ res.status(500).json({ hasResult: false, error: 'Failed to check status' });
3621
+ }
3622
+ });
3007
3623
  return router;
3008
3624
  }
3009
3625
  //# sourceMappingURL=routes.js.map