@payez/next-mvp 4.0.21 → 4.0.22

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,271 +1,271 @@
1
- /**
2
- * Better Auth Configuration
3
- *
4
- * Primary auth configuration. Replaces the former NextAuth auth-options.ts.
5
- *
6
- * Architecture: No database adapter — Better Auth runs in stateless mode
7
- * with JWE cookie cache. User management stays on IDP, sessions on Redis.
8
- *
9
- * @see BETTER-AUTH-MIGRATION-SPEC.md
10
- */
11
-
12
- import 'server-only';
13
- import { betterAuth } from 'better-auth';
14
- import { nextCookies } from 'better-auth/next-js';
15
- import { toNextJsHandler } from 'better-auth/next-js';
16
- import type { IDPClientConfig } from '../lib/idp-client-config';
17
- import { getIDPClientConfig } from '../lib/idp-client-config';
18
- import { getAppSlug } from '../lib/app-slug';
19
- import { getRedis } from '../lib/redis';
20
-
21
- /**
22
- * Better Auth social provider config shape.
23
- */
24
- export interface BetterAuthSocialProvider {
25
- clientId: string;
26
- clientSecret: string;
27
- scope?: string[];
28
- }
29
-
30
- /**
31
- * Build Better Auth social providers from IDP config.
32
- */
33
- export function buildBetterAuthProviders(
34
- config: IDPClientConfig
35
- ): Record<string, BetterAuthSocialProvider> {
36
- const providers: Record<string, BetterAuthSocialProvider> = {};
37
-
38
- for (const oauth of config.oauthProviders || []) {
39
- if (!oauth.enabled) continue;
40
- const name = oauth.provider.toLowerCase();
41
- providers[name] = {
42
- clientId: oauth.clientId,
43
- clientSecret: oauth.clientSecret,
44
- scope: oauth.scopes?.split(' '),
45
- };
46
- }
47
-
48
- return providers;
49
- }
50
-
51
- /**
52
- * Create Better Auth instance from IDP config.
53
- *
54
- * No database — runs in stateless mode with JWE cookie cache.
55
- * Call after getIDPClientConfig() resolves.
56
- */
57
- export function createBetterAuthInstance(idpConfig: IDPClientConfig) {
58
- const appSlug = idpConfig.clientSlug || getAppSlug();
59
-
60
- // Resolve base URL: BETTER_AUTH_URL env > IDP config > localhost fallback
61
- const baseURL = process.env.BETTER_AUTH_URL
62
- || idpConfig.baseClientUrl
63
- || `http://localhost:${process.env.PORT || '3000'}`;
64
-
65
- return betterAuth({
66
- baseURL,
67
- secret: idpConfig.nextAuthSecret as string,
68
-
69
- socialProviders: buildBetterAuthProviders(idpConfig),
70
-
71
- // Trust the app's own origin + any configured base URL
72
- trustedOrigins: [
73
- baseURL,
74
- ...(idpConfig.baseClientUrl && idpConfig.baseClientUrl !== baseURL ? [idpConfig.baseClientUrl] : []),
75
- 'http://localhost:3000',
76
- 'http://localhost:3400',
77
- 'http://localhost:3600',
78
- ],
79
-
80
- // Redis-backed session storage via secondaryStorage
81
- secondaryStorage: {
82
- get: async (key: string) => {
83
- try {
84
- return await getRedis().get(`ba:${appSlug}:${key}`);
85
- } catch { return null; }
86
- },
87
- set: async (key: string, value: string, ttl?: number) => {
88
- try {
89
- const redis = getRedis();
90
- if (ttl) {
91
- await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
92
- } else {
93
- await redis.setex(`ba:${appSlug}:${key}`, 7 * 24 * 60 * 60, value);
94
- }
95
- } catch { /* Redis unavailable — cookie cache still works */ }
96
- },
97
- delete: async (key: string) => {
98
- try {
99
- await getRedis().del(`ba:${appSlug}:${key}`);
100
- } catch { /* ignore */ }
101
- },
102
- },
103
-
104
- session: {
105
- cookieCache: {
106
- enabled: true,
107
- maxAge: 300,
108
- refreshCache: false,
109
- },
110
- },
111
-
112
- // After social login, exchange Google identity for IDP tokens and store in Redis
113
- databaseHooks: {
114
- session: {
115
- create: {
116
- after: async (session: any) => {
117
- try {
118
- const userId = session.userId;
119
- const token = session.token;
120
- if (!userId || !token) return;
121
-
122
- // Look up user from Better Auth's memory/DB to get email
123
- // The user was just created/found by Better Auth during OAuth
124
- const baKey = `ba:${appSlug}:${token}`;
125
- const baRaw = await getRedis().get(baKey).catch(() => null);
126
- const baData = baRaw ? JSON.parse(baRaw) : null;
127
- const email = baData?.user?.email;
128
- const name = baData?.user?.name;
129
- const image = baData?.user?.image;
130
-
131
- if (!email) {
132
- console.warn('[BETTER_AUTH] Session created but no email found for IDP token exchange');
133
- return;
134
- }
135
-
136
- // Call IDP oauth-callback to get IDP tokens
137
- const idpUrl = process.env.IDP_URL || '';
138
- if (!idpUrl) {
139
- console.warn('[BETTER_AUTH] No IDP URL configured, skipping token exchange');
140
- return;
141
- }
142
-
143
- const oauthRes = await fetch(`${idpUrl}/api/ExternalAuth/oauth-callback`, {
144
- method: 'POST',
145
- headers: { 'Content-Type': 'application/json' },
146
- body: JSON.stringify({
147
- provider: 'google',
148
- provider_account_id: userId,
149
- email,
150
- name,
151
- image,
152
- client_id: idpConfig.clientSlug || String(idpConfig.clientId),
153
- }),
154
- });
155
-
156
- const oauthResText = await oauthRes.text();
157
- console.log('[BETTER_AUTH] IDP oauth-callback response:', oauthRes.status, oauthResText.substring(0, 500));
158
-
159
- if (!oauthRes.ok) {
160
- console.error('[BETTER_AUTH] IDP oauth-callback failed:', oauthRes.status);
161
- return;
162
- }
163
-
164
- let idpData: any;
165
- try { idpData = JSON.parse(oauthResText); } catch { return; }
166
- const result = idpData?.data?.result || idpData?.data || idpData;
167
-
168
- if (!result?.access_token) {
169
- console.warn('[BETTER_AUTH] IDP oauth-callback returned no access_token. Keys:', Object.keys(result || {}));
170
- return;
171
- }
172
-
173
- // Store IDP tokens in the BA Redis session
174
- if (baData) {
175
- const requiresTwoFactor = result.user?.requiresTwoFactor ?? result.requiresTwoFactor ?? false;
176
- baData.idpTokens = {
177
- idpAccessToken: result.access_token,
178
- idpRefreshToken: result.refresh_token,
179
- idpAccessTokenExpires: result.expires_in
180
- ? Date.now() + result.expires_in * 1000
181
- : Date.now() + 15 * 60 * 1000,
182
- userId: String(result.user?.user_id || result.user?.id || result.user_id || userId),
183
- email: result.user?.email || result.email || email,
184
- name: result.user?.full_name || result.user?.name || result.name || name,
185
- roles: result.user?.roles || result.roles || [],
186
- mfaVerified: !requiresTwoFactor,
187
- };
188
- await getRedis().setex(baKey, 7 * 24 * 60 * 60, JSON.stringify(baData));
189
- console.log('[BETTER_AUTH] IDP tokens stored in session for', email);
190
- }
191
- } catch (err) {
192
- console.error('[BETTER_AUTH] Post-login IDP exchange failed:', err instanceof Error ? err.message : String(err));
193
- }
194
- },
195
- },
196
- },
197
- },
198
-
199
- // Cookie prefix must match slim-middleware expectations ({slug}.session-token)
200
- advanced: {
201
- cookiePrefix: appSlug,
202
- cookies: {
203
- session_token: {
204
- name: `${appSlug}.session-token`,
205
- },
206
- },
207
- },
208
-
209
- plugins: [
210
- nextCookies(),
211
- ],
212
- });
213
- }
214
-
215
- /**
216
- * Better Auth is always enabled (NextAuth removed in 4.0).
217
- */
218
- export function isBetterAuthEnabled(): boolean {
219
- return true;
220
- }
221
-
222
- /**
223
- * Get Better Auth Next.js route handlers (GET, POST).
224
- * Initializes Better Auth from IDP config on first call, caches the instance.
225
- */
226
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
227
- let cachedInstance: any = null;
228
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
229
- let initPromise: Promise<any> | null = null;
230
-
231
- // Expose for server-side session access (decode-session.ts)
232
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
233
- export { cachedInstance as __betterAuthInstance };
234
-
235
- export async function getBetterAuthInstance() {
236
- if (cachedInstance) return cachedInstance;
237
-
238
- if (!initPromise) {
239
- initPromise = getIDPClientConfig().then(config => {
240
- const instance = createBetterAuthInstance(config);
241
- cachedInstance = instance;
242
- console.log('[BETTER_AUTH] Instance created for', config.clientSlug || config.clientId);
243
- return instance;
244
- });
245
- }
246
-
247
- return initPromise;
248
- }
249
-
250
- /**
251
- * Get flag-gated auth handler for Next.js route.
252
- *
253
- * When USE_BETTER_AUTH=true, returns Better Auth handlers.
254
- * Otherwise returns null (auth disabled).
255
- *
256
- * Usage in host app route:
257
- * ```ts
258
- * import { getBetterAuthHandler } from '@payez/next-mvp/auth/better-auth';
259
- *
260
- * export async function GET(req: Request) {
261
- * const ba = await getBetterAuthHandler();
262
- * if (ba) return ba.GET(req);
263
- * }
264
- * ```
265
- */
266
- export async function getBetterAuthHandler(): Promise<{ GET: (req: Request) => Promise<Response>; POST: (req: Request) => Promise<Response> } | null> {
267
- if (!isBetterAuthEnabled()) return null;
268
-
269
- const auth = await getBetterAuthInstance();
270
- return toNextJsHandler(auth);
271
- }
1
+ /**
2
+ * Better Auth Configuration
3
+ *
4
+ * Primary auth configuration. Replaces the former NextAuth auth-options.ts.
5
+ *
6
+ * Architecture: No database adapter — Better Auth runs in stateless mode
7
+ * with JWE cookie cache. User management stays on IDP, sessions on Redis.
8
+ *
9
+ * @see BETTER-AUTH-MIGRATION-SPEC.md
10
+ */
11
+
12
+ import 'server-only';
13
+ import { betterAuth } from 'better-auth';
14
+ import { nextCookies } from 'better-auth/next-js';
15
+ import { toNextJsHandler } from 'better-auth/next-js';
16
+ import type { IDPClientConfig } from '../lib/idp-client-config';
17
+ import { getIDPClientConfig } from '../lib/idp-client-config';
18
+ import { getAppSlug } from '../lib/app-slug';
19
+ import { getRedis } from '../lib/redis';
20
+
21
+ /**
22
+ * Better Auth social provider config shape.
23
+ */
24
+ export interface BetterAuthSocialProvider {
25
+ clientId: string;
26
+ clientSecret: string;
27
+ scope?: string[];
28
+ }
29
+
30
+ /**
31
+ * Build Better Auth social providers from IDP config.
32
+ */
33
+ export function buildBetterAuthProviders(
34
+ config: IDPClientConfig
35
+ ): Record<string, BetterAuthSocialProvider> {
36
+ const providers: Record<string, BetterAuthSocialProvider> = {};
37
+
38
+ for (const oauth of config.oauthProviders || []) {
39
+ if (!oauth.enabled) continue;
40
+ const name = oauth.provider.toLowerCase();
41
+ providers[name] = {
42
+ clientId: oauth.clientId,
43
+ clientSecret: oauth.clientSecret,
44
+ scope: oauth.scopes?.split(' '),
45
+ };
46
+ }
47
+
48
+ return providers;
49
+ }
50
+
51
+ /**
52
+ * Create Better Auth instance from IDP config.
53
+ *
54
+ * No database — runs in stateless mode with JWE cookie cache.
55
+ * Call after getIDPClientConfig() resolves.
56
+ */
57
+ export function createBetterAuthInstance(idpConfig: IDPClientConfig) {
58
+ const appSlug = idpConfig.clientSlug || getAppSlug();
59
+
60
+ // Resolve base URL: BETTER_AUTH_URL env > IDP config > localhost fallback
61
+ const baseURL = process.env.BETTER_AUTH_URL
62
+ || idpConfig.baseClientUrl
63
+ || `http://localhost:${process.env.PORT || '3000'}`;
64
+
65
+ return betterAuth({
66
+ baseURL,
67
+ secret: idpConfig.nextAuthSecret as string,
68
+
69
+ socialProviders: buildBetterAuthProviders(idpConfig),
70
+
71
+ // Trust the app's own origin + any configured base URL
72
+ trustedOrigins: [
73
+ baseURL,
74
+ ...(idpConfig.baseClientUrl && idpConfig.baseClientUrl !== baseURL ? [idpConfig.baseClientUrl] : []),
75
+ 'http://localhost:3000',
76
+ 'http://localhost:3400',
77
+ 'http://localhost:3600',
78
+ ],
79
+
80
+ // Redis-backed session storage via secondaryStorage
81
+ secondaryStorage: {
82
+ get: async (key: string) => {
83
+ try {
84
+ return await getRedis().get(`ba:${appSlug}:${key}`);
85
+ } catch { return null; }
86
+ },
87
+ set: async (key: string, value: string, ttl?: number) => {
88
+ try {
89
+ const redis = getRedis();
90
+ if (ttl) {
91
+ await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
92
+ } else {
93
+ await redis.setex(`ba:${appSlug}:${key}`, 7 * 24 * 60 * 60, value);
94
+ }
95
+ } catch { /* Redis unavailable — cookie cache still works */ }
96
+ },
97
+ delete: async (key: string) => {
98
+ try {
99
+ await getRedis().del(`ba:${appSlug}:${key}`);
100
+ } catch { /* ignore */ }
101
+ },
102
+ },
103
+
104
+ session: {
105
+ cookieCache: {
106
+ enabled: true,
107
+ maxAge: 300,
108
+ refreshCache: false,
109
+ },
110
+ },
111
+
112
+ // After social login, exchange Google identity for IDP tokens and store in Redis
113
+ databaseHooks: {
114
+ session: {
115
+ create: {
116
+ after: async (session: any) => {
117
+ try {
118
+ const userId = session.userId;
119
+ const token = session.token;
120
+ if (!userId || !token) return;
121
+
122
+ // Look up user from Better Auth's memory/DB to get email
123
+ // The user was just created/found by Better Auth during OAuth
124
+ const baKey = `ba:${appSlug}:${token}`;
125
+ const baRaw = await getRedis().get(baKey).catch(() => null);
126
+ const baData = baRaw ? JSON.parse(baRaw) : null;
127
+ const email = baData?.user?.email;
128
+ const name = baData?.user?.name;
129
+ const image = baData?.user?.image;
130
+
131
+ if (!email) {
132
+ console.warn('[BETTER_AUTH] Session created but no email found for IDP token exchange');
133
+ return;
134
+ }
135
+
136
+ // Call IDP oauth-callback to get IDP tokens
137
+ const idpUrl = process.env.IDP_URL || '';
138
+ if (!idpUrl) {
139
+ console.warn('[BETTER_AUTH] No IDP URL configured, skipping token exchange');
140
+ return;
141
+ }
142
+
143
+ const oauthRes = await fetch(`${idpUrl}/api/ExternalAuth/oauth-callback`, {
144
+ method: 'POST',
145
+ headers: { 'Content-Type': 'application/json' },
146
+ body: JSON.stringify({
147
+ provider: 'google',
148
+ provider_account_id: userId,
149
+ email,
150
+ name,
151
+ image,
152
+ client_id: idpConfig.clientSlug || String(idpConfig.clientId),
153
+ }),
154
+ });
155
+
156
+ const oauthResText = await oauthRes.text();
157
+ console.log('[BETTER_AUTH] IDP oauth-callback response:', oauthRes.status, oauthResText.substring(0, 500));
158
+
159
+ if (!oauthRes.ok) {
160
+ console.error('[BETTER_AUTH] IDP oauth-callback failed:', oauthRes.status);
161
+ return;
162
+ }
163
+
164
+ let idpData: any;
165
+ try { idpData = JSON.parse(oauthResText); } catch { return; }
166
+ const result = idpData?.data?.result || idpData?.data || idpData;
167
+
168
+ if (!result?.access_token) {
169
+ console.warn('[BETTER_AUTH] IDP oauth-callback returned no access_token. Keys:', Object.keys(result || {}));
170
+ return;
171
+ }
172
+
173
+ // Store IDP tokens in the BA Redis session
174
+ if (baData) {
175
+ const requiresTwoFactor = result.user?.requiresTwoFactor ?? result.requiresTwoFactor ?? false;
176
+ baData.idpTokens = {
177
+ idpAccessToken: result.access_token,
178
+ idpRefreshToken: result.refresh_token,
179
+ idpAccessTokenExpires: result.expires_in
180
+ ? Date.now() + result.expires_in * 1000
181
+ : Date.now() + 15 * 60 * 1000,
182
+ userId: String(result.user?.user_id || result.user?.id || result.user_id || userId),
183
+ email: result.user?.email || result.email || email,
184
+ name: result.user?.full_name || result.user?.name || result.name || name,
185
+ roles: result.user?.roles || result.roles || [],
186
+ mfaVerified: !requiresTwoFactor,
187
+ };
188
+ await getRedis().setex(baKey, 7 * 24 * 60 * 60, JSON.stringify(baData));
189
+ console.log('[BETTER_AUTH] IDP tokens stored in session for', email);
190
+ }
191
+ } catch (err) {
192
+ console.error('[BETTER_AUTH] Post-login IDP exchange failed:', err instanceof Error ? err.message : String(err));
193
+ }
194
+ },
195
+ },
196
+ },
197
+ },
198
+
199
+ // Cookie prefix must match slim-middleware expectations ({slug}.session-token)
200
+ advanced: {
201
+ cookiePrefix: appSlug,
202
+ cookies: {
203
+ session_token: {
204
+ name: `${appSlug}.session-token`,
205
+ },
206
+ },
207
+ },
208
+
209
+ plugins: [
210
+ nextCookies(),
211
+ ],
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Better Auth is always enabled (NextAuth removed in 4.0).
217
+ */
218
+ export function isBetterAuthEnabled(): boolean {
219
+ return true;
220
+ }
221
+
222
+ /**
223
+ * Get Better Auth Next.js route handlers (GET, POST).
224
+ * Initializes Better Auth from IDP config on first call, caches the instance.
225
+ */
226
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
227
+ let cachedInstance: any = null;
228
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
229
+ let initPromise: Promise<any> | null = null;
230
+
231
+ // Expose for server-side session access (decode-session.ts)
232
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
233
+ export { cachedInstance as __betterAuthInstance };
234
+
235
+ export async function getBetterAuthInstance() {
236
+ if (cachedInstance) return cachedInstance;
237
+
238
+ if (!initPromise) {
239
+ initPromise = getIDPClientConfig(true).then(config => {
240
+ const instance = createBetterAuthInstance(config);
241
+ cachedInstance = instance;
242
+ console.log('[BETTER_AUTH] Instance created for', config.clientSlug || config.clientId);
243
+ return instance;
244
+ });
245
+ }
246
+
247
+ return initPromise;
248
+ }
249
+
250
+ /**
251
+ * Get flag-gated auth handler for Next.js route.
252
+ *
253
+ * When USE_BETTER_AUTH=true, returns Better Auth handlers.
254
+ * Otherwise returns null (auth disabled).
255
+ *
256
+ * Usage in host app route:
257
+ * ```ts
258
+ * import { getBetterAuthHandler } from '@payez/next-mvp/auth/better-auth';
259
+ *
260
+ * export async function GET(req: Request) {
261
+ * const ba = await getBetterAuthHandler();
262
+ * if (ba) return ba.GET(req);
263
+ * }
264
+ * ```
265
+ */
266
+ export async function getBetterAuthHandler(): Promise<{ GET: (req: Request) => Promise<Response>; POST: (req: Request) => Promise<Response> } | null> {
267
+ if (!isBetterAuthEnabled()) return null;
268
+
269
+ const auth = await getBetterAuthInstance();
270
+ return toNextJsHandler(auth);
271
+ }
@@ -15,7 +15,7 @@ const defaultConfig: AuthConfig = {
15
15
  allowPasswordReset: true,
16
16
  };
17
17
 
18
- // Map NextAuth provider IDs to our FederatedProvider type
18
+ // Map provider IDs to our FederatedProvider type
19
19
  const PROVIDER_MAP: Record<string, FederatedProvider> = {
20
20
  'google': 'google',
21
21
  'apple': 'apple',
@@ -32,9 +32,8 @@ interface AuthProviderProps {
32
32
  children: ReactNode;
33
33
  config?: Partial<AuthConfig>;
34
34
  /**
35
- * If true, providers will be fetched dynamically from NextAuth
35
+ * If true, providers will be loaded dynamically from IDP config
36
36
  * instead of using the static providers array from config.
37
- * Defaults to true for dynamic provider loading from IDP.
38
37
  */
39
38
  useDynamicProviders?: boolean;
40
39
  }
@@ -54,7 +53,7 @@ export function AuthProvider({ children, config, useDynamicProviders = true }: A
54
53
  },
55
54
  }));
56
55
 
57
- // Fetch dynamic providers from NextAuth on mount
56
+ // Load available providers on mount
58
57
  useEffect(() => {
59
58
  if (!useDynamicProviders) return;
60
59
 
@@ -18,6 +18,20 @@ export const authClient = createAuthClient({
18
18
  // Convenience exports
19
19
  export const { useSession, signIn, signOut } = authClient;
20
20
 
21
+ /**
22
+ * Sign in with a specific OAuth provider via direct redirect.
23
+ *
24
+ * better-auth exposes per-provider endpoints at /api/auth/sign-in/{provider}
25
+ * (e.g. /api/auth/sign-in/google). The generic signIn.social() method POSTs
26
+ * to /api/auth/sign-in/social which doesn't exist — use this instead.
27
+ */
28
+ export function signInWithProvider(provider: string, callbackURL?: string) {
29
+ const params = new URLSearchParams();
30
+ if (callbackURL) params.set('callbackURL', callbackURL);
31
+ const qs = params.toString();
32
+ window.location.href = `/api/auth/sign-in/${provider}${qs ? '?' + qs : ''}`;
33
+ }
34
+
21
35
  /**
22
36
  * NextAuth-compatible useSession wrapper.
23
37
  *
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useEffect, useCallback } from 'react';
4
- import { authClient } from '../../client/better-auth-client';
4
+ import { authClient, signInWithProvider } from '../../client/better-auth-client';
5
5
  import { usePathname } from 'next/navigation';
6
6
  import Image from 'next/image';
7
7
  import Link from 'next/link';
@@ -90,7 +90,7 @@ export function MobileNavDrawer({
90
90
  if (onSignIn) {
91
91
  onSignIn();
92
92
  } else {
93
- authClient.signIn.social({ provider: 'google', callbackURL: signInCallbackUrl });
93
+ signInWithProvider('google', signInCallbackUrl);
94
94
  }
95
95
  };
96
96
 
@@ -1,17 +1,16 @@
1
1
  /**
2
2
  * useAvailableProviders Hook
3
3
  *
4
- * Fetches the list of OAuth providers actually configured in NextAuth.
5
- * This ensures UI only shows buttons for providers that are enabled in IDP.
4
+ * Returns the list of OAuth providers configured for this client.
5
+ * Ensures UI only shows buttons for providers that are enabled in IDP.
6
6
  */
7
7
 
8
8
  'use client';
9
9
 
10
10
  import { useState, useEffect } from 'react';
11
- import { authClient } from '../client/better-auth-client';
12
11
  import type { FederatedProvider } from '../types/auth';
13
12
 
14
- // Map NextAuth provider IDs to our FederatedProvider type
13
+ // Map provider IDs to our FederatedProvider type
15
14
  const PROVIDER_MAP: Record<string, FederatedProvider> = {
16
15
  'google': 'google',
17
16
  'apple': 'apple',
@@ -32,10 +31,9 @@ export interface UseAvailableProvidersResult {
32
31
  }
33
32
 
34
33
  /**
35
- * Hook to get available OAuth providers from NextAuth.
34
+ * Hook to get available federated OAuth providers.
36
35
  *
37
- * Returns only the providers that are actually configured in auth-options,
38
- * which reflects what's enabled in IDP config.
36
+ * Returns only the providers that are enabled in IDP config.
39
37
  *
40
38
  * @example
41
39
  * ```tsx