@thunderid/nextjs 1.0.8 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/ThunderIDNextClient.d.ts +16 -0
  2. package/dist/ThunderIDNextClient.d.ts.map +1 -1
  3. package/dist/ThunderIDNextClient.js +28 -2
  4. package/dist/ThunderIDNextClient.js.map +1 -1
  5. package/dist/cjs/ThunderIDNextClient.cjs +27 -1
  6. package/dist/cjs/ThunderIDNextClient.cjs.map +1 -1
  7. package/dist/cjs/client/components/presentation/ChangeCredential/ChangeCredential.cjs +104 -0
  8. package/dist/cjs/client/components/presentation/ChangeCredential/ChangeCredential.cjs.map +1 -0
  9. package/dist/cjs/client/components/presentation/UserDropdown/UserDropdown.cjs +8 -4
  10. package/dist/cjs/client/components/presentation/UserDropdown/UserDropdown.cjs.map +1 -1
  11. package/dist/cjs/client/contexts/ThunderID/ThunderIDProvider.cjs +2 -3
  12. package/dist/cjs/client/contexts/ThunderID/ThunderIDProvider.cjs.map +1 -1
  13. package/dist/cjs/client/index.cjs +1 -0
  14. package/dist/cjs/index.cjs +4 -0
  15. package/dist/cjs/server/actions/handleOAuthCallbackAction.cjs +2 -4
  16. package/dist/cjs/server/actions/handleOAuthCallbackAction.cjs.map +1 -1
  17. package/dist/cjs/server/actions/signInAction.cjs +1 -1
  18. package/dist/cjs/server/actions/signInAction.cjs.map +1 -1
  19. package/dist/cjs/server/actions/updateUserCredentialsAction.cjs +41 -0
  20. package/dist/cjs/server/actions/updateUserCredentialsAction.cjs.map +1 -0
  21. package/dist/cjs/server/backchannelLogout.cjs +92 -0
  22. package/dist/cjs/server/backchannelLogout.cjs.map +1 -0
  23. package/dist/cjs/server/index.cjs +2 -0
  24. package/dist/cjs/server/proxy/thunderIDProxy.cjs +4 -1
  25. package/dist/cjs/server/proxy/thunderIDProxy.cjs.map +1 -1
  26. package/dist/cjs/utils/SessionManager.cjs +17 -1
  27. package/dist/cjs/utils/SessionManager.cjs.map +1 -1
  28. package/dist/cjs/utils/handleRefreshToken.cjs +5 -2
  29. package/dist/cjs/utils/handleRefreshToken.cjs.map +1 -1
  30. package/dist/cjs/utils/loggedOutSessions.cjs +32 -0
  31. package/dist/cjs/utils/loggedOutSessions.cjs.map +1 -0
  32. package/dist/client/components/presentation/ChangeCredential/ChangeCredential.d.ts +45 -0
  33. package/dist/client/components/presentation/ChangeCredential/ChangeCredential.d.ts.map +1 -0
  34. package/dist/client/components/presentation/ChangeCredential/ChangeCredential.js +99 -0
  35. package/dist/client/components/presentation/ChangeCredential/ChangeCredential.js.map +1 -0
  36. package/dist/client/components/presentation/UserDropdown/UserDropdown.d.ts +12 -0
  37. package/dist/client/components/presentation/UserDropdown/UserDropdown.d.ts.map +1 -1
  38. package/dist/client/components/presentation/UserDropdown/UserDropdown.js +8 -4
  39. package/dist/client/components/presentation/UserDropdown/UserDropdown.js.map +1 -1
  40. package/dist/client/contexts/ThunderID/ThunderIDProvider.d.ts +1 -1
  41. package/dist/client/contexts/ThunderID/ThunderIDProvider.d.ts.map +1 -1
  42. package/dist/client/contexts/ThunderID/ThunderIDProvider.js +3 -4
  43. package/dist/client/contexts/ThunderID/ThunderIDProvider.js.map +1 -1
  44. package/dist/client/index.d.ts +2 -0
  45. package/dist/client/index.d.ts.map +1 -1
  46. package/dist/client/index.js +1 -0
  47. package/dist/index.js +3 -1
  48. package/dist/server/actions/handleOAuthCallbackAction.d.ts +1 -2
  49. package/dist/server/actions/handleOAuthCallbackAction.d.ts.map +1 -1
  50. package/dist/server/actions/handleOAuthCallbackAction.js +2 -4
  51. package/dist/server/actions/handleOAuthCallbackAction.js.map +1 -1
  52. package/dist/server/actions/signInAction.d.ts.map +1 -1
  53. package/dist/server/actions/signInAction.js +1 -1
  54. package/dist/server/actions/signInAction.js.map +1 -1
  55. package/dist/server/actions/updateUserCredentialsAction.d.ts +16 -0
  56. package/dist/server/actions/updateUserCredentialsAction.d.ts.map +1 -0
  57. package/dist/server/actions/updateUserCredentialsAction.js +39 -0
  58. package/dist/server/actions/updateUserCredentialsAction.js.map +1 -0
  59. package/dist/server/backchannelLogout.d.ts +40 -0
  60. package/dist/server/backchannelLogout.d.ts.map +1 -0
  61. package/dist/server/backchannelLogout.js +90 -0
  62. package/dist/server/backchannelLogout.js.map +1 -0
  63. package/dist/server/index.d.ts +2 -0
  64. package/dist/server/index.d.ts.map +1 -1
  65. package/dist/server/index.js +2 -1
  66. package/dist/server/proxy/thunderIDProxy.d.ts +12 -1
  67. package/dist/server/proxy/thunderIDProxy.d.ts.map +1 -1
  68. package/dist/server/proxy/thunderIDProxy.js +4 -1
  69. package/dist/server/proxy/thunderIDProxy.js.map +1 -1
  70. package/dist/utils/SessionManager.d.ts +13 -1
  71. package/dist/utils/SessionManager.d.ts.map +1 -1
  72. package/dist/utils/SessionManager.js +17 -1
  73. package/dist/utils/SessionManager.js.map +1 -1
  74. package/dist/utils/handleRefreshToken.d.ts.map +1 -1
  75. package/dist/utils/handleRefreshToken.js +5 -2
  76. package/dist/utils/handleRefreshToken.js.map +1 -1
  77. package/dist/utils/loggedOutSessions.d.ts +12 -0
  78. package/dist/utils/loggedOutSessions.d.ts.map +1 -0
  79. package/dist/utils/loggedOutSessions.js +29 -0
  80. package/dist/utils/loggedOutSessions.js.map +1 -0
  81. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"ThunderIDProvider.cjs","names":["ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps>>","reRenderCheckRef: RefObject<boolean>","router: AppRouterInstance","searchParams: ReadonlyURLSearchParams","code: string | null","state: string | null","sessionState: string | null","result: {error?: string; redirectUrl?: string; success: boolean}","ThunderIDRuntimeError","result: any","contextValue: ThunderIDContextProps","reactContextValue: ReactThunderIDContextProps","ThunderIDContext","ReactThunderIDContext","I18nProvider","FlowMetaProvider","ThemeProvider","FlowProvider","UserProvider"],"sources":["../../../../../src/client/contexts/ThunderID/ThunderIDProvider.tsx"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\n'use client';\n\nimport {\n AttributeSchema,\n EmbeddedFlowExecuteRequestConfig,\n FlowMetadataResponse,\n generateFlattenedUserProfile,\n UpdateMeProfileConfig,\n User,\n UserProfile,\n ThunderIDRuntimeError,\n getVendorPrefix,\n} from '@thunderid/node';\nimport {\n I18nProvider,\n FlowMetaProvider,\n FlowProvider,\n UserProvider,\n ThemeProvider,\n ThunderIDContext as ReactThunderIDContext,\n ThunderIDContextProps as ReactThunderIDContextProps,\n ThunderIDProviderProps,\n getActiveTheme,\n} from '@thunderid/react';\nimport {ReadonlyURLSearchParams} from 'next/dist/client/components/navigation.react-server';\nimport {AppRouterInstance} from 'next/dist/shared/lib/app-router-context.shared-runtime';\nimport {useRouter, useSearchParams} from 'next/navigation';\nimport {FC, PropsWithChildren, RefObject, useEffect, useMemo, useRef, useState} from 'react';\nimport ThunderIDContext, {ThunderIDContextProps} from './ThunderIDContext';\nimport {RefreshResult} from '../../../server/actions/refreshToken';\nimport logger from '../../../utils/logger';\n\n/**\n * Props interface of {@link ThunderIDClientProvider}\n */\nexport type ThunderIDClientProviderProps = Partial<Omit<ThunderIDProviderProps, 'baseUrl' | 'clientId'>> &\n Pick<ThunderIDProviderProps, 'baseUrl' | 'clientId'> & {\n applicationId: ThunderIDContextProps['applicationId'];\n clearSession: () => Promise<void>;\n /**\n * Server Action that fetches `GET /flow/meta` server-side, passed down to `FlowMetaProvider` so\n * it never calls `baseUrl` directly from the browser — avoiding the CORS requirement that a\n * direct client-side fetch to the ThunderID server would otherwise impose. Used for both the\n * initial fetch (when `initialMeta` isn't seeded, e.g. it failed server-side) and subsequent\n * `switchLanguage()` calls.\n */\n fetchMeta?: (params: {applicationId?: string; language?: string}) => Promise<FlowMetadataResponse>;\n handleOAuthCallback: (\n code: string,\n state: string,\n sessionState?: string,\n ) => Promise<{error?: string; redirectUrl?: string; success: boolean}>;\n /**\n * Flow metadata fetched server-side ahead of time, seeding `FlowMetaProvider` so it can skip\n * its own initial client-side fetch (avoiding a flash of untranslated i18n keys).\n */\n initialMeta?: FlowMetadataResponse | null;\n isSignedIn: boolean;\n organizationHandle: ThunderIDContextProps['organizationHandle'];\n refreshToken: () => Promise<RefreshResult>;\n signIn: ThunderIDContextProps['signIn'];\n signOut: ThunderIDContextProps['signOut'];\n signUp: ThunderIDContextProps['signUp'];\n updateProfile: (\n requestConfig: UpdateMeProfileConfig,\n sessionId?: string,\n ) => Promise<{data: {user: User}; error: string; success: boolean}>;\n user: User | null;\n userProfile: UserProfile;\n userSchema?: Record<string, AttributeSchema> | null;\n };\n\nconst ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps>> = ({\n baseUrl,\n children,\n signIn,\n clearSession,\n refreshToken,\n signOut,\n signUp,\n handleOAuthCallback,\n preferences,\n isSignedIn,\n signInUrl,\n signUpUrl,\n user: _user,\n userProfile: _userProfile,\n userSchema: _userSchema = null,\n updateProfile,\n applicationId,\n organizationHandle,\n scopes,\n vendor,\n initialMeta = null,\n fetchMeta,\n}: PropsWithChildren<ThunderIDClientProviderProps>) => {\n const reRenderCheckRef: RefObject<boolean> = useRef(false);\n const router: AppRouterInstance = useRouter();\n const searchParams: ReadonlyURLSearchParams = useSearchParams();\n const [isLoading, setIsLoading] = useState<boolean>(true);\n const [user, setUser] = useState<User | null>(_user);\n const [userProfile, setUserProfile] = useState<UserProfile>(_userProfile);\n const [userSchema, setUserSchema] = useState<Record<string, AttributeSchema> | null>(_userSchema);\n\n useEffect(() => {\n setUserProfile(_userProfile);\n }, [_userProfile]);\n\n useEffect(() => {\n setUserSchema(_userSchema);\n }, [_userSchema]);\n\n useEffect(() => {\n setUser(_user);\n }, [_user]);\n\n // Handle OAuth callback automatically\n useEffect(() => {\n // React 18.x Strict.Mode has a new check for `Ensuring reusable state` to facilitate an upcoming react feature.\n // https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state\n // This will remount all the useEffects to ensure that there are no unexpected side effects.\n // When react remounts the signIn hook of the AuthProvider, it will cause a race condition. Hence, we have to\n // prevent the re-render of this hook as suggested in the following discussion.\n // https://github.com/reactwg/react-18/discussions/18#discussioncomment-795623\n if (reRenderCheckRef.current) {\n return;\n }\n\n reRenderCheckRef.current = true;\n\n // Don't handle callback if already signed in\n if (isSignedIn) return;\n\n (async (): Promise<void> => {\n try {\n const code: string | null = searchParams.get('code');\n const state: string | null = searchParams.get('state');\n const sessionState: string | null = searchParams.get('session_state');\n const error: string | null = searchParams.get('error');\n\n // Check for OAuth errors first\n if (error) {\n logger.error('[ThunderIDClientProvider] An error was received for the initiated sign-in request.');\n\n return;\n }\n\n // Handle OAuth callback if code and state are present\n if (code && state) {\n setIsLoading(true);\n\n const result: {error?: string; redirectUrl?: string; success: boolean} = await handleOAuthCallback(\n code,\n state,\n sessionState || undefined,\n );\n\n if (result.success) {\n // Redirect to the success URL\n if (result.redirectUrl) {\n router.push(result.redirectUrl);\n } else {\n // Refresh the page to update authentication state\n window.location.reload();\n }\n } else {\n logger.error(\n `[ThunderIDClientProvider] An error occurred while signing in: ${result.error || 'Authentication failed'}`,\n );\n }\n }\n } catch (error) {\n logger.error('[ThunderIDClientProvider] Failed to handle OAuth callback:', error);\n }\n })();\n }, []);\n\n useEffect(() => {\n // Set loading to false when server has resolved authentication state\n setIsLoading(false);\n }, [isSignedIn, user]);\n\n const handleSignIn = async (payload: any, request: EmbeddedFlowExecuteRequestConfig): Promise<any> => {\n if (!signIn) {\n throw new ThunderIDRuntimeError(\n '`signIn` function is not available.',\n 'ThunderIDClientProvider-handleSignIn-RuntimeError-001',\n 'nextjs',\n );\n }\n\n const result: any = await signIn(payload, request);\n\n // Redirect based flow URL is sent as `signInUrl` in the response.\n // Use window.location.href instead of router.push() — the OAuth authorization\n // endpoint is on an external server, and router.push() would send RSC fetch\n // headers that the identity provider doesn't understand, causing a CORS error.\n if (result?.data?.signInUrl) {\n window.location.href = result.data.signInUrl;\n\n return undefined;\n }\n\n // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignInUrl` in the response.\n if (result?.data?.afterSignInUrl) {\n router.push(result.data.afterSignInUrl);\n\n return undefined;\n }\n\n if (result?.error) {\n throw new Error(result.error);\n }\n\n return result?.data ?? result;\n };\n\n const handleSignUp = async (payload: any, request: EmbeddedFlowExecuteRequestConfig): Promise<any> => {\n if (!signUp) {\n throw new ThunderIDRuntimeError(\n '`signUp` function is not available.',\n 'ThunderIDClientProvider-handleSignUp-RuntimeError-001',\n 'nextjs',\n );\n }\n\n const result: any = await signUp(payload, request);\n\n // Redirect based flow URL is sent as `signUpUrl` in the response.\n if (result?.data?.signUpUrl) {\n router.push(result.data.signUpUrl);\n\n return undefined;\n }\n\n // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignUpUrl` in the response.\n if (result?.data?.afterSignUpUrl) {\n router.push(result.data.afterSignUpUrl);\n\n return undefined;\n }\n\n if (result?.error) {\n throw new Error(result.error);\n }\n\n return result?.data ?? result;\n };\n\n const handleSignOut = async (): Promise<any> => {\n logger.debug('[ThunderIDClientProvider][handleSignOut] `handleSignOut` called.');\n\n try {\n const result: any = await signOut();\n\n logger.debug('[ThunderIDClientProvider][handleSignOut] Sign out result:', result);\n\n if (result?.data?.afterSignOutUrl) {\n router.push(result.data.afterSignOutUrl);\n\n return {location: result.data.afterSignOutUrl, redirected: true};\n }\n\n if (result?.error) {\n logger.error(\n '[ThunderIDClientProvider][handleSignOut] Error result was returned during signing the user out with a button click:',\n result.error,\n );\n }\n\n return result?.data ?? result;\n } catch (error) {\n logger.error(\n '[ThunderIDClientProvider][handleSignOut] Error occurred during signing the user out with a button click:',\n error,\n );\n\n return undefined;\n }\n };\n\n const contextValue: ThunderIDContextProps = useMemo(\n () => ({\n applicationId,\n baseUrl,\n clearSession,\n isLoading,\n isSignedIn,\n organizationHandle,\n preferences,\n refreshToken,\n scopes,\n signIn: handleSignIn,\n signInUrl,\n signOut: handleSignOut,\n signUp: handleSignUp,\n signUpUrl,\n user,\n }),\n [\n baseUrl,\n user,\n isSignedIn,\n isLoading,\n signInUrl,\n signUpUrl,\n applicationId,\n organizationHandle,\n scopes,\n preferences,\n ],\n );\n\n const handleProfileUpdate = (payload: User): void => {\n setUser(payload);\n setUserProfile((prev: UserProfile) => ({\n ...prev,\n flattenedProfile: generateFlattenedUserProfile(payload),\n profile: payload,\n }));\n };\n\n // Bridge into @thunderid/react's own ThunderIDContext. Internal react components rendered by\n // BaseSignIn/BaseSignUp — most notably FlowMetaProvider, which fetches `/flow/meta` and injects\n // its i18n bundle — call react's `useThunderID()` directly. Without this bridge they'd only see\n // react's context default (applicationId/baseUrl undefined, isInitialized false), so the meta\n // fetch would never fire and flow labels would render as untranslated i18n keys. Fields with no\n // nextjs equivalent (token/http helpers meant for direct browser API calls) are stubbed out,\n // since nextjs routes those operations through server actions instead.\n const unsupported = (name: string): (() => Promise<never>) => {\n return () =>\n Promise.reject(\n new ThunderIDRuntimeError(\n `\\`${name}\\` is not supported in @thunderid/nextjs.`,\n `ThunderIDClientProvider-${name}-NotSupportedError-001`,\n 'nextjs',\n ),\n );\n };\n\n const reactContextValue: ReactThunderIDContextProps = useMemo(\n () => ({\n afterSignInUrl: undefined,\n applicationId,\n baseUrl,\n clearSession,\n clientId: undefined,\n discovery: {wellKnown: null},\n exchangeToken: unsupported('exchangeToken'),\n getAccessToken: unsupported('getAccessToken'),\n getDecodedIdToken: unsupported('getDecodedIdToken'),\n getIdToken: unsupported('getIdToken'),\n getStorageManager: () => Promise.resolve(null),\n http: {\n request: unsupported('http.request'),\n requestAll: unsupported('http.requestAll'),\n },\n instanceId: 0,\n isInitialized: !isLoading,\n isLoading,\n isMetaLoading: false,\n isSignedIn,\n meta: initialMeta ?? null,\n organizationHandle,\n reInitialize: unsupported('reInitialize'),\n recover: unsupported('recover'),\n resolveFlowTemplateLiterals: (text: string | undefined): string => text ?? '',\n scopes,\n signIn: handleSignIn,\n signInSilently: unsupported('signInSilently'),\n signInUrl,\n signOut: handleSignOut,\n signUp: handleSignUp,\n signUpUrl,\n user,\n userSchema,\n vendor: getVendorPrefix(vendor),\n }),\n [\n applicationId,\n baseUrl,\n clearSession,\n isLoading,\n isSignedIn,\n organizationHandle,\n scopes,\n signInUrl,\n signUpUrl,\n user,\n userSchema,\n initialMeta,\n vendor,\n ],\n );\n\n return (\n <ThunderIDContext.Provider value={contextValue}>\n <ReactThunderIDContext.Provider value={reactContextValue}>\n <I18nProvider preferences={preferences?.i18n}>\n <FlowMetaProvider\n enabled={preferences?.resolveFromMeta !== false}\n initialMeta={initialMeta}\n fetchMeta={fetchMeta}\n >\n <ThemeProvider theme={preferences?.theme?.overrides} mode={getActiveTheme(preferences?.theme?.mode as any)}>\n <FlowProvider>\n <UserProvider\n profile={userProfile}\n userSchema={userSchema}\n onUpdateProfile={handleProfileUpdate}\n updateProfile={updateProfile}\n >\n {children}\n </UserProvider>\n </FlowProvider>\n </ThemeProvider>\n </FlowMetaProvider>\n </I18nProvider>\n </ReactThunderIDContext.Provider>\n </ThunderIDContext.Provider>\n );\n};\n\nexport default ThunderIDClientProvider;\n"],"mappings":"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"}
1
+ {"version":3,"file":"ThunderIDProvider.cjs","names":["ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps>>","reRenderCheckRef: RefObject<boolean>","router: AppRouterInstance","searchParams: ReadonlyURLSearchParams","code: string | null","state: string | null","result: {error?: string; redirectUrl?: string; success: boolean}","ThunderIDRuntimeError","result: any","contextValue: ThunderIDContextProps","reactContextValue: ReactThunderIDContextProps","ThunderIDContext","ReactThunderIDContext","I18nProvider","FlowMetaProvider","ThemeProvider","FlowProvider","UserProvider"],"sources":["../../../../../src/client/contexts/ThunderID/ThunderIDProvider.tsx"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\n'use client';\n\nimport {\n AttributeSchema,\n EmbeddedFlowExecuteRequestConfig,\n FlowMetadataResponse,\n generateFlattenedUserProfile,\n UpdateMeProfileConfig,\n User,\n UserProfile,\n ThunderIDRuntimeError,\n getVendorPrefix,\n} from '@thunderid/node';\nimport {\n I18nProvider,\n FlowMetaProvider,\n FlowProvider,\n UserProvider,\n ThemeProvider,\n ThunderIDContext as ReactThunderIDContext,\n ThunderIDContextProps as ReactThunderIDContextProps,\n ThunderIDProviderProps,\n} from '@thunderid/react';\nimport {ReadonlyURLSearchParams} from 'next/dist/client/components/navigation.react-server';\nimport {AppRouterInstance} from 'next/dist/shared/lib/app-router-context.shared-runtime';\nimport {useRouter, useSearchParams} from 'next/navigation';\nimport {FC, PropsWithChildren, RefObject, useEffect, useMemo, useRef, useState} from 'react';\nimport ThunderIDContext, {ThunderIDContextProps} from './ThunderIDContext';\nimport {RefreshResult} from '../../../server/actions/refreshToken';\nimport logger from '../../../utils/logger';\n\n/**\n * Props interface of {@link ThunderIDClientProvider}\n */\nexport type ThunderIDClientProviderProps = Partial<Omit<ThunderIDProviderProps, 'baseUrl' | 'clientId'>> &\n Pick<ThunderIDProviderProps, 'baseUrl' | 'clientId'> & {\n applicationId: ThunderIDContextProps['applicationId'];\n clearSession: () => Promise<void>;\n /**\n * Server Action that fetches `GET /flow/meta` server-side, passed down to `FlowMetaProvider` so\n * it never calls `baseUrl` directly from the browser — avoiding the CORS requirement that a\n * direct client-side fetch to the ThunderID server would otherwise impose. Used for both the\n * initial fetch (when `initialMeta` isn't seeded, e.g. it failed server-side) and subsequent\n * `switchLanguage()` calls.\n */\n fetchMeta?: (params: {applicationId?: string; language?: string}) => Promise<FlowMetadataResponse>;\n handleOAuthCallback: (\n code: string,\n state: string,\n ) => Promise<{error?: string; redirectUrl?: string; success: boolean}>;\n /**\n * Flow metadata fetched server-side ahead of time, seeding `FlowMetaProvider` so it can skip\n * its own initial client-side fetch (avoiding a flash of untranslated i18n keys).\n */\n initialMeta?: FlowMetadataResponse | null;\n isSignedIn: boolean;\n organizationHandle: ThunderIDContextProps['organizationHandle'];\n refreshToken: () => Promise<RefreshResult>;\n signIn: ThunderIDContextProps['signIn'];\n signOut: ThunderIDContextProps['signOut'];\n signUp: ThunderIDContextProps['signUp'];\n updateProfile: (\n requestConfig: UpdateMeProfileConfig,\n sessionId?: string,\n ) => Promise<{data: {user: User}; error: string; success: boolean}>;\n user: User | null;\n userProfile: UserProfile;\n userSchema?: Record<string, AttributeSchema> | null;\n };\n\nconst ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps>> = ({\n baseUrl,\n children,\n signIn,\n clearSession,\n refreshToken,\n signOut,\n signUp,\n handleOAuthCallback,\n preferences,\n isSignedIn,\n signInUrl,\n signUpUrl,\n user: _user,\n userProfile: _userProfile,\n userSchema: _userSchema = null,\n updateProfile,\n applicationId,\n organizationHandle,\n scopes,\n vendor,\n initialMeta = null,\n fetchMeta,\n}: PropsWithChildren<ThunderIDClientProviderProps>) => {\n const reRenderCheckRef: RefObject<boolean> = useRef(false);\n const router: AppRouterInstance = useRouter();\n const searchParams: ReadonlyURLSearchParams = useSearchParams();\n const [isLoading, setIsLoading] = useState<boolean>(true);\n const [user, setUser] = useState<User | null>(_user);\n const [userProfile, setUserProfile] = useState<UserProfile>(_userProfile);\n const [userSchema, setUserSchema] = useState<Record<string, AttributeSchema> | null>(_userSchema);\n\n useEffect(() => {\n setUserProfile(_userProfile);\n }, [_userProfile]);\n\n useEffect(() => {\n setUserSchema(_userSchema);\n }, [_userSchema]);\n\n useEffect(() => {\n setUser(_user);\n }, [_user]);\n\n // Handle OAuth callback automatically\n useEffect(() => {\n // React 18.x Strict.Mode has a new check for `Ensuring reusable state` to facilitate an upcoming react feature.\n // https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state\n // This will remount all the useEffects to ensure that there are no unexpected side effects.\n // When react remounts the signIn hook of the AuthProvider, it will cause a race condition. Hence, we have to\n // prevent the re-render of this hook as suggested in the following discussion.\n // https://github.com/reactwg/react-18/discussions/18#discussioncomment-795623\n if (reRenderCheckRef.current) {\n return;\n }\n\n reRenderCheckRef.current = true;\n\n // Don't handle callback if already signed in\n if (isSignedIn) return;\n\n (async (): Promise<void> => {\n try {\n const code: string | null = searchParams.get('code');\n const state: string | null = searchParams.get('state');\n const error: string | null = searchParams.get('error');\n\n // Check for OAuth errors first\n if (error) {\n logger.error('[ThunderIDClientProvider] An error was received for the initiated sign-in request.');\n\n return;\n }\n\n // Handle OAuth callback if code and state are present\n if (code && state) {\n setIsLoading(true);\n\n const result: {error?: string; redirectUrl?: string; success: boolean} = await handleOAuthCallback(\n code,\n state,\n );\n\n if (result.success) {\n // Redirect to the success URL\n if (result.redirectUrl) {\n router.push(result.redirectUrl);\n } else {\n // Refresh the page to update authentication state\n window.location.reload();\n }\n } else {\n logger.error(\n `[ThunderIDClientProvider] An error occurred while signing in: ${result.error || 'Authentication failed'}`,\n );\n }\n }\n } catch (error) {\n logger.error('[ThunderIDClientProvider] Failed to handle OAuth callback:', error);\n }\n })();\n }, []);\n\n useEffect(() => {\n // Set loading to false when server has resolved authentication state\n setIsLoading(false);\n }, [isSignedIn, user]);\n\n const handleSignIn = async (payload: any, request: EmbeddedFlowExecuteRequestConfig): Promise<any> => {\n if (!signIn) {\n throw new ThunderIDRuntimeError(\n '`signIn` function is not available.',\n 'ThunderIDClientProvider-handleSignIn-RuntimeError-001',\n 'nextjs',\n );\n }\n\n const result: any = await signIn(payload, request);\n\n // Redirect based flow URL is sent as `signInUrl` in the response.\n // Use window.location.href instead of router.push() — the OAuth authorization\n // endpoint is on an external server, and router.push() would send RSC fetch\n // headers that the identity provider doesn't understand, causing a CORS error.\n if (result?.data?.signInUrl) {\n window.location.href = result.data.signInUrl;\n\n return undefined;\n }\n\n // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignInUrl` in the response.\n if (result?.data?.afterSignInUrl) {\n router.push(result.data.afterSignInUrl);\n\n return undefined;\n }\n\n if (result?.error) {\n throw new Error(result.error);\n }\n\n return result?.data ?? result;\n };\n\n const handleSignUp = async (payload: any, request: EmbeddedFlowExecuteRequestConfig): Promise<any> => {\n if (!signUp) {\n throw new ThunderIDRuntimeError(\n '`signUp` function is not available.',\n 'ThunderIDClientProvider-handleSignUp-RuntimeError-001',\n 'nextjs',\n );\n }\n\n const result: any = await signUp(payload, request);\n\n // Redirect based flow URL is sent as `signUpUrl` in the response.\n if (result?.data?.signUpUrl) {\n router.push(result.data.signUpUrl);\n\n return undefined;\n }\n\n // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignUpUrl` in the response.\n if (result?.data?.afterSignUpUrl) {\n router.push(result.data.afterSignUpUrl);\n\n return undefined;\n }\n\n if (result?.error) {\n throw new Error(result.error);\n }\n\n return result?.data ?? result;\n };\n\n const handleSignOut = async (): Promise<any> => {\n logger.debug('[ThunderIDClientProvider][handleSignOut] `handleSignOut` called.');\n\n try {\n const result: any = await signOut();\n\n logger.debug('[ThunderIDClientProvider][handleSignOut] Sign out result:', result);\n\n if (result?.data?.afterSignOutUrl) {\n router.push(result.data.afterSignOutUrl);\n\n return {location: result.data.afterSignOutUrl, redirected: true};\n }\n\n if (result?.error) {\n logger.error(\n '[ThunderIDClientProvider][handleSignOut] Error result was returned during signing the user out with a button click:',\n result.error,\n );\n }\n\n return result?.data ?? result;\n } catch (error) {\n logger.error(\n '[ThunderIDClientProvider][handleSignOut] Error occurred during signing the user out with a button click:',\n error,\n );\n\n return undefined;\n }\n };\n\n const contextValue: ThunderIDContextProps = useMemo(\n () => ({\n applicationId,\n baseUrl,\n clearSession,\n isLoading,\n isSignedIn,\n organizationHandle,\n preferences,\n refreshToken,\n scopes,\n signIn: handleSignIn,\n signInUrl,\n signOut: handleSignOut,\n signUp: handleSignUp,\n signUpUrl,\n user,\n }),\n [\n baseUrl,\n user,\n isSignedIn,\n isLoading,\n signInUrl,\n signUpUrl,\n applicationId,\n organizationHandle,\n scopes,\n preferences,\n ],\n );\n\n const handleProfileUpdate = (payload: User): void => {\n setUser(payload);\n setUserProfile((prev: UserProfile) => ({\n ...prev,\n flattenedProfile: generateFlattenedUserProfile(payload),\n profile: payload,\n }));\n };\n\n // Bridge into @thunderid/react's own ThunderIDContext. Internal react components rendered by\n // BaseSignIn/BaseSignUp — most notably FlowMetaProvider, which fetches `/flow/meta` and injects\n // its i18n bundle — call react's `useThunderID()` directly. Without this bridge they'd only see\n // react's context default (applicationId/baseUrl undefined, isInitialized false), so the meta\n // fetch would never fire and flow labels would render as untranslated i18n keys. Fields with no\n // nextjs equivalent (token/http helpers meant for direct browser API calls) are stubbed out,\n // since nextjs routes those operations through server actions instead.\n const unsupported = (name: string): (() => Promise<never>) => {\n return () =>\n Promise.reject(\n new ThunderIDRuntimeError(\n `\\`${name}\\` is not supported in @thunderid/nextjs.`,\n `ThunderIDClientProvider-${name}-NotSupportedError-001`,\n 'nextjs',\n ),\n );\n };\n\n const reactContextValue: ReactThunderIDContextProps = useMemo(\n () => ({\n afterSignInUrl: undefined,\n applicationId,\n baseUrl,\n clearSession,\n clientId: undefined,\n discovery: {wellKnown: null},\n exchangeToken: unsupported('exchangeToken'),\n getAccessToken: unsupported('getAccessToken'),\n getDecodedIdToken: unsupported('getDecodedIdToken'),\n getIdToken: unsupported('getIdToken'),\n getStorageManager: () => Promise.resolve(null),\n http: {\n request: unsupported('http.request'),\n requestAll: unsupported('http.requestAll'),\n },\n instanceId: 0,\n isInitialized: !isLoading,\n isLoading,\n isMetaLoading: false,\n isSignedIn,\n meta: initialMeta ?? null,\n organizationHandle,\n reInitialize: unsupported('reInitialize'),\n recover: unsupported('recover'),\n resolveFlowTemplateLiterals: (text: string | undefined): string => text ?? '',\n scopes,\n signIn: handleSignIn,\n signInSilently: unsupported('signInSilently'),\n signInUrl,\n signOut: handleSignOut,\n signUp: handleSignUp,\n signUpUrl,\n user,\n userSchema,\n vendor: getVendorPrefix(vendor),\n }),\n [\n applicationId,\n baseUrl,\n clearSession,\n isLoading,\n isSignedIn,\n organizationHandle,\n scopes,\n signInUrl,\n signUpUrl,\n user,\n userSchema,\n initialMeta,\n vendor,\n ],\n );\n\n return (\n <ThunderIDContext.Provider value={contextValue}>\n <ReactThunderIDContext.Provider value={reactContextValue}>\n <I18nProvider preferences={preferences?.i18n}>\n <FlowMetaProvider\n enabled={preferences?.resolveFromMeta !== false}\n initialMeta={initialMeta}\n fetchMeta={fetchMeta}\n >\n <ThemeProvider theme={preferences?.theme?.overrides} mode={preferences?.theme?.mode}>\n <FlowProvider>\n <UserProvider\n profile={userProfile}\n userSchema={userSchema}\n onUpdateProfile={handleProfileUpdate}\n updateProfile={updateProfile}\n >\n {children}\n </UserProvider>\n </FlowProvider>\n </ThemeProvider>\n </FlowMetaProvider>\n </I18nProvider>\n </ReactThunderIDContext.Provider>\n </ThunderIDContext.Provider>\n );\n};\n\nexport default ThunderIDClientProvider;\n"],"mappings":";;;;;;;;;;;;;;;;;;AAyEA,MAAMA,2BAAgF,EACpF,SACA,UACA,QACA,cACA,cACA,SACA,QACA,qBACA,aACA,YACA,WACA,WACA,MAAM,OACN,aAAa,cACb,YAAY,cAAc,MAC1B,eACA,eACA,oBACA,QACA,QACA,cAAc,MACd,gBACqD;CACrD,MAAMC,qCAA8C,MAAM;CAC1D,MAAMC,yCAAuC;CAC7C,MAAMC,qDAAyD;CAC/D,MAAM,CAAC,WAAW,oCAAkC,KAAK;CACzD,MAAM,CAAC,MAAM,+BAAiC,MAAM;CACpD,MAAM,CAAC,aAAa,sCAAwC,aAAa;CACzE,MAAM,CAAC,YAAY,qCAAkE,YAAY;AAEjG,4BAAgB;AACd,iBAAe,aAAa;IAC3B,CAAC,aAAa,CAAC;AAElB,4BAAgB;AACd,gBAAc,YAAY;IACzB,CAAC,YAAY,CAAC;AAEjB,4BAAgB;AACd,UAAQ,MAAM;IACb,CAAC,MAAM,CAAC;AAGX,4BAAgB;AAOd,MAAI,iBAAiB,QACnB;AAGF,mBAAiB,UAAU;AAG3B,MAAI,WAAY;AAEhB,GAAC,YAA2B;AAC1B,OAAI;IACF,MAAMC,OAAsB,aAAa,IAAI,OAAO;IACpD,MAAMC,QAAuB,aAAa,IAAI,QAAQ;AAItD,QAH6B,aAAa,IAAI,QAAQ,EAG3C;AACT,4BAAO,MAAM,qFAAqF;AAElG;;AAIF,QAAI,QAAQ,OAAO;AACjB,kBAAa,KAAK;KAElB,MAAMC,SAAmE,MAAM,oBAC7E,MACA,MACD;AAED,SAAI,OAAO,QAET,KAAI,OAAO,YACT,QAAO,KAAK,OAAO,YAAY;SAG/B,QAAO,SAAS,QAAQ;SAG1B,wBAAO,MACL,iEAAiE,OAAO,SAAS,0BAClF;;YAGE,OAAO;AACd,2BAAO,MAAM,8DAA8D,MAAM;;MAEjF;IACH,EAAE,CAAC;AAEN,4BAAgB;AAEd,eAAa,MAAM;IAClB,CAAC,YAAY,KAAK,CAAC;CAEtB,MAAM,eAAe,OAAO,SAAc,YAA4D;AACpG,MAAI,CAAC,OACH,OAAM,IAAIC,uCACR,uCACA,yDACA,SACD;EAGH,MAAMC,SAAc,MAAM,OAAO,SAAS,QAAQ;AAMlD,MAAI,QAAQ,MAAM,WAAW;AAC3B,UAAO,SAAS,OAAO,OAAO,KAAK;AAEnC;;AAIF,MAAI,QAAQ,MAAM,gBAAgB;AAChC,UAAO,KAAK,OAAO,KAAK,eAAe;AAEvC;;AAGF,MAAI,QAAQ,MACV,OAAM,IAAI,MAAM,OAAO,MAAM;AAG/B,SAAO,QAAQ,QAAQ;;CAGzB,MAAM,eAAe,OAAO,SAAc,YAA4D;AACpG,MAAI,CAAC,OACH,OAAM,IAAID,uCACR,uCACA,yDACA,SACD;EAGH,MAAMC,SAAc,MAAM,OAAO,SAAS,QAAQ;AAGlD,MAAI,QAAQ,MAAM,WAAW;AAC3B,UAAO,KAAK,OAAO,KAAK,UAAU;AAElC;;AAIF,MAAI,QAAQ,MAAM,gBAAgB;AAChC,UAAO,KAAK,OAAO,KAAK,eAAe;AAEvC;;AAGF,MAAI,QAAQ,MACV,OAAM,IAAI,MAAM,OAAO,MAAM;AAG/B,SAAO,QAAQ,QAAQ;;CAGzB,MAAM,gBAAgB,YAA0B;AAC9C,yBAAO,MAAM,mEAAmE;AAEhF,MAAI;GACF,MAAMA,SAAc,MAAM,SAAS;AAEnC,0BAAO,MAAM,6DAA6D,OAAO;AAEjF,OAAI,QAAQ,MAAM,iBAAiB;AACjC,WAAO,KAAK,OAAO,KAAK,gBAAgB;AAExC,WAAO;KAAC,UAAU,OAAO,KAAK;KAAiB,YAAY;KAAK;;AAGlE,OAAI,QAAQ,MACV,wBAAO,MACL,uHACA,OAAO,MACR;AAGH,UAAO,QAAQ,QAAQ;WAChB,OAAO;AACd,0BAAO,MACL,4GACA,MACD;AAED;;;CAIJ,MAAMC,yCACG;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA,SAAS;EACT,QAAQ;EACR;EACA;EACD,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;CAED,MAAM,uBAAuB,YAAwB;AACnD,UAAQ,QAAQ;AAChB,kBAAgB,UAAuB;GACrC,GAAG;GACH,qEAA+C,QAAQ;GACvD,SAAS;GACV,EAAE;;CAUL,MAAM,eAAe,SAAyC;AAC5D,eACE,QAAQ,OACN,IAAIF,uCACF,KAAK,KAAK,4CACV,2BAA2B,KAAK,yBAChC,SACD,CACF;;CAGL,MAAMG,8CACG;EACL,gBAAgB;EAChB;EACA;EACA;EACA,UAAU;EACV,WAAW,EAAC,WAAW,MAAK;EAC5B,eAAe,YAAY,gBAAgB;EAC3C,gBAAgB,YAAY,iBAAiB;EAC7C,mBAAmB,YAAY,oBAAoB;EACnD,YAAY,YAAY,aAAa;EACrC,yBAAyB,QAAQ,QAAQ,KAAK;EAC9C,MAAM;GACJ,SAAS,YAAY,eAAe;GACpC,YAAY,YAAY,kBAAkB;GAC3C;EACD,YAAY;EACZ,eAAe,CAAC;EAChB;EACA,eAAe;EACf;EACA,MAAM,eAAe;EACrB;EACA,cAAc,YAAY,eAAe;EACzC,SAAS,YAAY,UAAU;EAC/B,8BAA8B,SAAqC,QAAQ;EAC3E;EACA,QAAQ;EACR,gBAAgB,YAAY,iBAAiB;EAC7C;EACA,SAAS;EACT,QAAQ;EACR;EACA;EACA;EACA,8CAAwB,OAAO;EAChC,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QACE,2CAACC,iCAAiB;EAAS,OAAO;YAChC,2CAACC,mCAAsB;GAAS,OAAO;aACrC,2CAACC;IAAa,aAAa,aAAa;cACtC,2CAACC;KACC,SAAS,aAAa,oBAAoB;KAC7B;KACF;eAEX,2CAACC;MAAc,OAAO,aAAa,OAAO;MAAW,MAAM,aAAa,OAAO;gBAC7E,2CAACC,4CACC,2CAACC;OACC,SAAS;OACG;OACZ,iBAAiB;OACF;OAEd;QACY,GACF;OACD;MACC;KACN;IACgB;GACP;;AAIhC,gCAAe"}
@@ -10,3 +10,4 @@ const require_SignUp = require('./components/presentation/SignUp/SignUp.cjs');
10
10
  const require_UserProfile = require('./components/presentation/UserProfile/UserProfile.cjs');
11
11
  const require_UserDropdown = require('./components/presentation/UserDropdown/UserDropdown.cjs');
12
12
  const require_UserAvatar = require('./components/presentation/UserAvatar/UserAvatar.cjs');
13
+ const require_ChangeCredential = require('./components/presentation/ChangeCredential/ChangeCredential.cjs');
@@ -3,6 +3,7 @@ const require_thunderid = require('./server/thunderid.cjs');
3
3
  const require_ThunderIDProvider = require('./server/ThunderIDProvider.cjs');
4
4
  const require_thunderIDProxy = require('./server/proxy/thunderIDProxy.cjs');
5
5
  const require_createRouteMatcher = require('./server/proxy/createRouteMatcher.cjs');
6
+ const require_backchannelLogout = require('./server/backchannelLogout.cjs');
6
7
  require('./server/index.cjs');
7
8
  const require_useThunderID = require('./client/contexts/ThunderID/useThunderID.cjs');
8
9
  const require_SignedIn = require('./client/components/control/SignedIn/SignedIn.cjs');
@@ -16,8 +17,10 @@ const require_SignUp = require('./client/components/presentation/SignUp/SignUp.c
16
17
  const require_UserProfile = require('./client/components/presentation/UserProfile/UserProfile.cjs');
17
18
  const require_UserDropdown = require('./client/components/presentation/UserDropdown/UserDropdown.cjs');
18
19
  const require_UserAvatar = require('./client/components/presentation/UserAvatar/UserAvatar.cjs');
20
+ const require_ChangeCredential = require('./client/components/presentation/ChangeCredential/ChangeCredential.cjs');
19
21
  require('./client/index.cjs');
20
22
 
23
+ exports.ChangeCredential = require_ChangeCredential.default;
21
24
  exports.SignIn = require_SignIn.default;
22
25
  exports.SignInButton = require_SignInButton.default;
23
26
  exports.SignOutButton = require_SignOutButton.default;
@@ -31,6 +34,7 @@ exports.User = require_User.default;
31
34
  exports.UserAvatar = require_UserAvatar.default;
32
35
  exports.UserDropdown = require_UserDropdown.default;
33
36
  exports.UserProfile = require_UserProfile.default;
37
+ exports.backchannelLogout = require_backchannelLogout.default;
34
38
  exports.createRouteMatcher = require_createRouteMatcher.default;
35
39
  exports.removeChunkedCookieFromHeader = require_thunderIDProxy.removeChunkedCookieFromHeader;
36
40
  exports.replaceChunkedCookieInHeader = require_thunderIDProxy.replaceChunkedCookieInHeader;
@@ -17,10 +17,9 @@ next_headers = require_rolldown_runtime.__toESM(next_headers);
17
17
  *
18
18
  * @param code - Authorization code from OAuth provider
19
19
  * @param state - State parameter from OAuth provider for CSRF protection
20
- * @param sessionState - Session state parameter from OAuth provider
21
20
  * @returns Promise that resolves with success status and optional error message
22
21
  */
23
- const handleOAuthCallbackAction = async (code, state, sessionState) => {
22
+ const handleOAuthCallbackAction = async (code, state) => {
24
23
  try {
25
24
  if (!code || !state) return {
26
25
  error: "Missing required OAuth parameters: code and state are required",
@@ -48,7 +47,6 @@ const handleOAuthCallbackAction = async (code, state, sessionState) => {
48
47
  }
49
48
  const signInResult = await thunderIDClient.signIn({
50
49
  code,
51
- session_state: sessionState,
52
50
  state
53
51
  }, {}, sessionId);
54
52
  const config = await thunderIDClient.getConfiguration();
@@ -61,7 +59,7 @@ const handleOAuthCallbackAction = async (code, state, sessionState) => {
61
59
  const organizationId = idToken["user_org"] || idToken["organization_id"];
62
60
  const expiresIn = signInResult["expiresIn"];
63
61
  const sessionCookieExpiryTime = require_SessionManager.default.resolveSessionCookieExpiry(config.sessionCookie?.expiryTime);
64
- const sessionToken = await require_SessionManager.default.createSessionToken(accessToken, userIdFromToken, sessionId, scopes, expiresIn, refreshToken, organizationId);
62
+ const sessionToken = await require_SessionManager.default.createSessionToken(accessToken, userIdFromToken, sessionId, scopes, expiresIn, refreshToken, organizationId, { sid: typeof idToken["sid"] === "string" ? idToken["sid"] : void 0 });
65
63
  require_chunkedCookie.setChunkedCookie(cookieStore, require_SessionManager.default.getSessionCookieName(), sessionToken, require_SessionManager.default.getSessionCookieOptions(sessionCookieExpiryTime));
66
64
  cookieStore.delete(require_SessionManager.default.getTempSessionCookieName());
67
65
  } catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"handleOAuthCallbackAction.cjs","names":["getClient","cookieStore: RequestCookies","sessionId: string | undefined","tempSessionToken: string | undefined","SessionManager","signInResult: Record<string, unknown>","config: ThunderIDNextConfig","idToken: IdToken","accessToken: string","refreshToken: string","userIdFromToken: string","scopes: string","organizationId: string | undefined","expiresIn: number","sessionCookieExpiryTime: number","sessionToken: string"],"sources":["../../../../src/server/actions/handleOAuthCallbackAction.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\n'use server';\n\nimport {IdToken} from '@thunderid/node';\nimport {cookies} from 'next/headers';\nimport {ThunderIDNextConfig} from '../../models/config';\nimport {setChunkedCookie} from '../../utils/chunkedCookie';\nimport logger from '../../utils/logger';\nimport SessionManager from '../../utils/SessionManager';\nimport getClient from '../getClient';\n\ntype RequestCookies = Awaited<ReturnType<typeof cookies>>;\n\n/**\n * Server action to handle OAuth callback with authorization code.\n * This action processes the authorization code received from the OAuth provider\n * and exchanges it for tokens to complete the authentication flow.\n *\n * @param code - Authorization code from OAuth provider\n * @param state - State parameter from OAuth provider for CSRF protection\n * @param sessionState - Session state parameter from OAuth provider\n * @returns Promise that resolves with success status and optional error message\n */\nconst handleOAuthCallbackAction = async (\n code: string,\n state: string,\n sessionState?: string,\n): Promise<{\n error?: string;\n redirectUrl?: string;\n success: boolean;\n}> => {\n try {\n if (!code || !state) {\n return {\n error: 'Missing required OAuth parameters: code and state are required',\n success: false,\n };\n }\n\n const thunderIDClient = getClient();\n\n if (!thunderIDClient.isInitialized) {\n return {\n error: 'ThunderID client is not initialized',\n success: false,\n };\n }\n\n const cookieStore: RequestCookies = await cookies();\n let sessionId: string | undefined;\n\n const tempSessionToken: string | undefined = cookieStore.get(SessionManager.getTempSessionCookieName())?.value;\n\n if (tempSessionToken) {\n try {\n const tempSession: {sessionId: string} = await SessionManager.verifyTempSession(tempSessionToken);\n sessionId = tempSession.sessionId;\n } catch {\n logger.error(\n '[handleOAuthCallbackAction] Invalid temporary session token, falling back to session ID from cookies.',\n );\n }\n }\n\n if (!sessionId) {\n logger.error('[handleOAuthCallbackAction] No session ID found in cookies or temporary session token.');\n\n return {\n error: 'No session found. Please start the authentication flow again.',\n success: false,\n };\n }\n\n // Exchange the authorization code for tokens\n const signInResult: Record<string, unknown> = await thunderIDClient.signIn(\n {\n code,\n session_state: sessionState,\n state,\n } as any,\n {},\n sessionId,\n );\n\n const config: ThunderIDNextConfig = await thunderIDClient.getConfiguration();\n\n if (signInResult) {\n try {\n const idToken: IdToken = await thunderIDClient.getDecodedIdToken(\n sessionId,\n (signInResult['id_token'] || signInResult['idToken']) as string,\n );\n const accessToken: string = (signInResult['accessToken'] || signInResult['access_token']) as string;\n const refreshToken: string = (signInResult['refreshToken'] as string | undefined) ?? '';\n const userIdFromToken: string = (idToken.sub || signInResult['sub'] || sessionId) as string;\n const scopes: string = signInResult['scope'] as string;\n const organizationId: string | undefined = (idToken['user_org'] || idToken['organization_id']) as\n | string\n | undefined;\n const expiresIn: number = signInResult['expiresIn'] as number;\n const sessionCookieExpiryTime: number = SessionManager.resolveSessionCookieExpiry(\n config.sessionCookie?.expiryTime,\n );\n\n const sessionToken: string = await SessionManager.createSessionToken(\n accessToken,\n userIdFromToken,\n sessionId,\n scopes,\n expiresIn,\n refreshToken,\n organizationId,\n );\n\n setChunkedCookie(\n cookieStore,\n SessionManager.getSessionCookieName(),\n sessionToken,\n SessionManager.getSessionCookieOptions(sessionCookieExpiryTime),\n );\n\n cookieStore.delete(SessionManager.getTempSessionCookieName());\n } catch (error) {\n logger.error(\n `[handleOAuthCallbackAction] Failed to create JWT session, continuing with legacy session:\n ${typeof error === 'string' ? error : JSON.stringify(error)}`,\n );\n }\n }\n\n const afterSignInUrl: string = config.afterSignInUrl || '/';\n\n return {\n redirectUrl: afterSignInUrl,\n success: true,\n };\n } catch (error) {\n let errorMessage = 'Authentication failed';\n\n if (error instanceof Error) {\n errorMessage = error.message;\n } else if (error && typeof error === 'object' && 'message' in error) {\n errorMessage = String((error as {message: unknown}).message);\n } else if (typeof error === 'string') {\n errorMessage = error;\n }\n\n return {\n error: errorMessage,\n success: false,\n };\n }\n};\n\nexport default handleOAuthCallbackAction;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAyBA,MAAM,4BAA4B,OAChC,MACA,OACA,iBAKI;AACJ,KAAI;AACF,MAAI,CAAC,QAAQ,CAAC,MACZ,QAAO;GACL,OAAO;GACP,SAAS;GACV;EAGH,MAAM,kBAAkBA,2BAAW;AAEnC,MAAI,CAAC,gBAAgB,cACnB,QAAO;GACL,OAAO;GACP,SAAS;GACV;EAGH,MAAMC,cAA8B,iCAAe;EACnD,IAAIC;EAEJ,MAAMC,mBAAuC,YAAY,IAAIC,+BAAe,0BAA0B,CAAC,EAAE;AAEzG,MAAI,iBACF,KAAI;AAEF,gBADyC,MAAMA,+BAAe,kBAAkB,iBAAiB,EACzE;UAClB;AACN,0BAAO,MACL,wGACD;;AAIL,MAAI,CAAC,WAAW;AACd,0BAAO,MAAM,yFAAyF;AAEtG,UAAO;IACL,OAAO;IACP,SAAS;IACV;;EAIH,MAAMC,eAAwC,MAAM,gBAAgB,OAClE;GACE;GACA,eAAe;GACf;GACD,EACD,EAAE,EACF,UACD;EAED,MAAMC,SAA8B,MAAM,gBAAgB,kBAAkB;AAE5E,MAAI,aACF,KAAI;GACF,MAAMC,UAAmB,MAAM,gBAAgB,kBAC7C,WACC,aAAa,eAAe,aAAa,WAC3C;GACD,MAAMC,cAAuB,aAAa,kBAAkB,aAAa;GACzE,MAAMC,eAAwB,aAAa,mBAA0C;GACrF,MAAMC,kBAA2B,QAAQ,OAAO,aAAa,UAAU;GACvE,MAAMC,SAAiB,aAAa;GACpC,MAAMC,iBAAsC,QAAQ,eAAe,QAAQ;GAG3E,MAAMC,YAAoB,aAAa;GACvC,MAAMC,0BAAkCV,+BAAe,2BACrD,OAAO,eAAe,WACvB;GAED,MAAMW,eAAuB,MAAMX,+BAAe,mBAChD,aACA,iBACA,WACA,QACA,WACA,cACA,eACD;AAED,0CACE,aACAA,+BAAe,sBAAsB,EACrC,cACAA,+BAAe,wBAAwB,wBAAwB,CAChE;AAED,eAAY,OAAOA,+BAAe,0BAA0B,CAAC;WACtD,OAAO;AACd,0BAAO,MACL;YACE,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,MAAM,GAC5D;;AAML,SAAO;GACL,aAH6B,OAAO,kBAAkB;GAItD,SAAS;GACV;UACM,OAAO;EACd,IAAI,eAAe;AAEnB,MAAI,iBAAiB,MACnB,gBAAe,MAAM;WACZ,SAAS,OAAO,UAAU,YAAY,aAAa,MAC5D,gBAAe,OAAQ,MAA6B,QAAQ;WACnD,OAAO,UAAU,SAC1B,gBAAe;AAGjB,SAAO;GACL,OAAO;GACP,SAAS;GACV;;;AAIL,wCAAe"}
1
+ {"version":3,"file":"handleOAuthCallbackAction.cjs","names":["getClient","cookieStore: RequestCookies","sessionId: string | undefined","tempSessionToken: string | undefined","SessionManager","signInResult: Record<string, unknown>","config: ThunderIDNextConfig","idToken: IdToken","accessToken: string","refreshToken: string","userIdFromToken: string","scopes: string","organizationId: string | undefined","expiresIn: number","sessionCookieExpiryTime: number","sessionToken: string"],"sources":["../../../../src/server/actions/handleOAuthCallbackAction.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\n'use server';\n\nimport {IdToken} from '@thunderid/node';\nimport {cookies} from 'next/headers';\nimport {ThunderIDNextConfig} from '../../models/config';\nimport {setChunkedCookie} from '../../utils/chunkedCookie';\nimport logger from '../../utils/logger';\nimport SessionManager from '../../utils/SessionManager';\nimport getClient from '../getClient';\n\ntype RequestCookies = Awaited<ReturnType<typeof cookies>>;\n\n/**\n * Server action to handle OAuth callback with authorization code.\n * This action processes the authorization code received from the OAuth provider\n * and exchanges it for tokens to complete the authentication flow.\n *\n * @param code - Authorization code from OAuth provider\n * @param state - State parameter from OAuth provider for CSRF protection\n * @returns Promise that resolves with success status and optional error message\n */\nconst handleOAuthCallbackAction = async (\n code: string,\n state: string,\n): Promise<{\n error?: string;\n redirectUrl?: string;\n success: boolean;\n}> => {\n try {\n if (!code || !state) {\n return {\n error: 'Missing required OAuth parameters: code and state are required',\n success: false,\n };\n }\n\n const thunderIDClient = getClient();\n\n if (!thunderIDClient.isInitialized) {\n return {\n error: 'ThunderID client is not initialized',\n success: false,\n };\n }\n\n const cookieStore: RequestCookies = await cookies();\n let sessionId: string | undefined;\n\n const tempSessionToken: string | undefined = cookieStore.get(SessionManager.getTempSessionCookieName())?.value;\n\n if (tempSessionToken) {\n try {\n const tempSession: {sessionId: string} = await SessionManager.verifyTempSession(tempSessionToken);\n sessionId = tempSession.sessionId;\n } catch {\n logger.error(\n '[handleOAuthCallbackAction] Invalid temporary session token, falling back to session ID from cookies.',\n );\n }\n }\n\n if (!sessionId) {\n logger.error('[handleOAuthCallbackAction] No session ID found in cookies or temporary session token.');\n\n return {\n error: 'No session found. Please start the authentication flow again.',\n success: false,\n };\n }\n\n // Exchange the authorization code for tokens\n const signInResult: Record<string, unknown> = await thunderIDClient.signIn(\n {\n code,\n state,\n } as any,\n {},\n sessionId,\n );\n\n const config: ThunderIDNextConfig = await thunderIDClient.getConfiguration();\n\n if (signInResult) {\n try {\n const idToken: IdToken = await thunderIDClient.getDecodedIdToken(\n sessionId,\n (signInResult['id_token'] || signInResult['idToken']) as string,\n );\n const accessToken: string = (signInResult['accessToken'] || signInResult['access_token']) as string;\n const refreshToken: string = (signInResult['refreshToken'] as string | undefined) ?? '';\n const userIdFromToken: string = (idToken.sub || signInResult['sub'] || sessionId) as string;\n const scopes: string = signInResult['scope'] as string;\n const organizationId: string | undefined = (idToken['user_org'] || idToken['organization_id']) as\n | string\n | undefined;\n const expiresIn: number = signInResult['expiresIn'] as number;\n const sessionCookieExpiryTime: number = SessionManager.resolveSessionCookieExpiry(\n config.sessionCookie?.expiryTime,\n );\n\n const sessionToken: string = await SessionManager.createSessionToken(\n accessToken,\n userIdFromToken,\n sessionId,\n scopes,\n expiresIn,\n refreshToken,\n organizationId,\n {sid: typeof idToken['sid'] === 'string' ? idToken['sid'] : undefined},\n );\n\n setChunkedCookie(\n cookieStore,\n SessionManager.getSessionCookieName(),\n sessionToken,\n SessionManager.getSessionCookieOptions(sessionCookieExpiryTime),\n );\n\n cookieStore.delete(SessionManager.getTempSessionCookieName());\n } catch (error) {\n logger.error(\n `[handleOAuthCallbackAction] Failed to create JWT session, continuing with legacy session:\n ${typeof error === 'string' ? error : JSON.stringify(error)}`,\n );\n }\n }\n\n const afterSignInUrl: string = config.afterSignInUrl || '/';\n\n return {\n redirectUrl: afterSignInUrl,\n success: true,\n };\n } catch (error) {\n let errorMessage = 'Authentication failed';\n\n if (error instanceof Error) {\n errorMessage = error.message;\n } else if (error && typeof error === 'object' && 'message' in error) {\n errorMessage = String((error as {message: unknown}).message);\n } else if (typeof error === 'string') {\n errorMessage = error;\n }\n\n return {\n error: errorMessage,\n success: false,\n };\n }\n};\n\nexport default handleOAuthCallbackAction;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAwBA,MAAM,4BAA4B,OAChC,MACA,UAKI;AACJ,KAAI;AACF,MAAI,CAAC,QAAQ,CAAC,MACZ,QAAO;GACL,OAAO;GACP,SAAS;GACV;EAGH,MAAM,kBAAkBA,2BAAW;AAEnC,MAAI,CAAC,gBAAgB,cACnB,QAAO;GACL,OAAO;GACP,SAAS;GACV;EAGH,MAAMC,cAA8B,iCAAe;EACnD,IAAIC;EAEJ,MAAMC,mBAAuC,YAAY,IAAIC,+BAAe,0BAA0B,CAAC,EAAE;AAEzG,MAAI,iBACF,KAAI;AAEF,gBADyC,MAAMA,+BAAe,kBAAkB,iBAAiB,EACzE;UAClB;AACN,0BAAO,MACL,wGACD;;AAIL,MAAI,CAAC,WAAW;AACd,0BAAO,MAAM,yFAAyF;AAEtG,UAAO;IACL,OAAO;IACP,SAAS;IACV;;EAIH,MAAMC,eAAwC,MAAM,gBAAgB,OAClE;GACE;GACA;GACD,EACD,EAAE,EACF,UACD;EAED,MAAMC,SAA8B,MAAM,gBAAgB,kBAAkB;AAE5E,MAAI,aACF,KAAI;GACF,MAAMC,UAAmB,MAAM,gBAAgB,kBAC7C,WACC,aAAa,eAAe,aAAa,WAC3C;GACD,MAAMC,cAAuB,aAAa,kBAAkB,aAAa;GACzE,MAAMC,eAAwB,aAAa,mBAA0C;GACrF,MAAMC,kBAA2B,QAAQ,OAAO,aAAa,UAAU;GACvE,MAAMC,SAAiB,aAAa;GACpC,MAAMC,iBAAsC,QAAQ,eAAe,QAAQ;GAG3E,MAAMC,YAAoB,aAAa;GACvC,MAAMC,0BAAkCV,+BAAe,2BACrD,OAAO,eAAe,WACvB;GAED,MAAMW,eAAuB,MAAMX,+BAAe,mBAChD,aACA,iBACA,WACA,QACA,WACA,cACA,gBACA,EAAC,KAAK,OAAO,QAAQ,WAAW,WAAW,QAAQ,SAAS,QAAU,CACvE;AAED,0CACE,aACAA,+BAAe,sBAAsB,EACrC,cACAA,+BAAe,wBAAwB,wBAAwB,CAChE;AAED,eAAY,OAAOA,+BAAe,0BAA0B,CAAC;WACtD,OAAO;AACd,0BAAO,MACL;YACE,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,MAAM,GAC5D;;AAML,SAAO;GACL,aAH6B,OAAO,kBAAkB;GAItD,SAAS;GACV;UACM,OAAO;EACd,IAAI,eAAe;AAEnB,MAAI,iBAAiB,MACnB,gBAAe,MAAM;WACZ,SAAS,OAAO,UAAU,YAAY,aAAa,MAC5D,gBAAe,OAAQ,MAA6B,QAAQ;WACnD,OAAO,UAAU,SAC1B,gBAAe;AAGjB,SAAO;GACL,OAAO;GACP,SAAS;GACV;;;AAIL,wCAAe"}
@@ -59,7 +59,7 @@ const signInAction = async (payload, request) => {
59
59
  const expiresIn = Math.max(exp - iat, 0);
60
60
  const config = await client.getConfiguration();
61
61
  const sessionCookieExpiryTime = require_SessionManager.default.resolveSessionCookieExpiry(config.sessionCookie?.expiryTime);
62
- const sessionToken = await require_SessionManager.default.createSessionToken(response.assertion, userIdFromToken, sessionId, scopes, expiresIn, "", organizationId);
62
+ const sessionToken = await require_SessionManager.default.createSessionToken(response.assertion, userIdFromToken, sessionId, scopes, expiresIn, "", organizationId, { sid: typeof idToken["sid"] === "string" ? idToken["sid"] : void 0 });
63
63
  require_chunkedCookie.setChunkedCookie(cookieStore, require_SessionManager.default.getSessionCookieName(), sessionToken, require_SessionManager.default.getSessionCookieOptions(sessionCookieExpiryTime));
64
64
  cookieStore.delete(require_SessionManager.default.getTempSessionCookieName());
65
65
  const afterSignInUrl = await (await client.getStorageManager()).getConfigDataParameter("afterSignInUrl");
@@ -1 +1 @@
1
- {"version":3,"file":"signInAction.cjs","names":["getClient","cookieStore: RequestCookies","sessionId: string | undefined","existingSessionToken: string | undefined","getChunkedCookie","SessionManager","tempSessionToken: string | undefined","tempSessionToken: string","defaultSignInUrl: string","response: any","EmbeddedSignInFlowStatus","idToken: IdToken","userIdFromToken: string","organizationId: string | undefined","scopes: string","iat: number","exp: number","expiresIn: number","config: ThunderIDNextConfig","sessionCookieExpiryTime: number","sessionToken: string","afterSignInUrl: string"],"sources":["../../../../src/server/actions/signInAction.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\n'use server';\n\nimport {\n generateSessionId,\n EmbeddedSignInFlowStatus,\n EmbeddedFlowExecuteRequestConfig,\n IdToken,\n isEmpty,\n} from '@thunderid/node';\nimport {cookies} from 'next/headers';\nimport {ThunderIDNextConfig} from '../../models/config';\nimport {getChunkedCookie, setChunkedCookie} from '../../utils/chunkedCookie';\nimport logger from '../../utils/logger';\nimport SessionManager, {SessionTokenPayload} from '../../utils/SessionManager';\nimport getClient from '../getClient';\n\ntype RequestCookies = Awaited<ReturnType<typeof cookies>>;\n\n/**\n * Server action for signing in a user.\n * Handles the embedded sign-in flow and manages session cookies.\n *\n * @param payload - The embedded sign-in flow payload\n * @param request - The embedded flow execute request config\n * @returns Promise that resolves when sign-in is complete\n */\nconst signInAction = async (\n payload?: any,\n request?: EmbeddedFlowExecuteRequestConfig,\n): Promise<{\n data?:\n | {\n afterSignInUrl?: string;\n signInUrl?: string;\n }\n | Record<string, unknown>;\n error?: string;\n success: boolean;\n}> => {\n try {\n const client = getClient();\n const cookieStore: RequestCookies = await cookies();\n\n let sessionId: string | undefined;\n\n const existingSessionToken: string | undefined = getChunkedCookie(\n cookieStore,\n SessionManager.getSessionCookieName(),\n );\n\n if (existingSessionToken) {\n try {\n const sessionPayload: SessionTokenPayload = await SessionManager.verifySessionToken(existingSessionToken);\n sessionId = sessionPayload.sessionId;\n } catch {\n // Invalid session token, will create new temp session\n }\n }\n\n if (!sessionId) {\n const tempSessionToken: string | undefined = cookieStore.get(SessionManager.getTempSessionCookieName())?.value;\n\n if (tempSessionToken) {\n try {\n const tempSession: {sessionId: string} = await SessionManager.verifyTempSession(tempSessionToken);\n sessionId = tempSession.sessionId;\n } catch {\n // Invalid temp session, will create new one\n }\n }\n }\n\n if (!sessionId) {\n sessionId = generateSessionId();\n\n const tempSessionToken: string = await SessionManager.createTempSession(sessionId);\n\n cookieStore.set(\n SessionManager.getTempSessionCookieName(),\n tempSessionToken,\n SessionManager.getTempSessionCookieOptions(),\n );\n }\n\n // If no payload provided, redirect to sign-in URL for redirect-based sign-in.\n if (!payload || isEmpty(payload)) {\n const defaultSignInUrl: string = await client.getAuthorizeRequestUrl({}, sessionId);\n return {data: {signInUrl: String(defaultSignInUrl)}, success: true};\n }\n\n // Handle embedded sign-in flow\n const response: any = await client.signIn(payload, request!, sessionId);\n\n if (response.flowStatus === EmbeddedSignInFlowStatus.Complete) {\n // On the V2 platform, a completed embedded flow (without a preceding `/authorize` redirect,\n // i.e. no `authId`) returns a self-contained JWT `assertion` rather than an authorization\n // code to exchange. This mirrors how @thunderid/react and @thunderid/vue treat it: the\n // assertion is used directly as the bearer session token.\n if (!response.assertion) {\n throw new Error('[signInAction] Flow completed without an assertion.');\n }\n\n const idToken: IdToken = await client.getDecodedIdToken(sessionId, response.assertion);\n const userIdFromToken: string = (idToken.sub as string | undefined) ?? sessionId;\n const organizationId: string | undefined = (idToken['user_org'] || idToken['organization_id']) as\n | string\n | undefined;\n const scopes: string = (idToken['scope'] as string | undefined) ?? '';\n const iat: number = (idToken['iat'] as number | undefined) ?? Math.floor(Date.now() / 1000);\n const exp: number = (idToken['exp'] as number | undefined) ?? iat + 3600;\n const expiresIn: number = Math.max(exp - iat, 0);\n\n const config: ThunderIDNextConfig = await client.getConfiguration();\n const sessionCookieExpiryTime: number = SessionManager.resolveSessionCookieExpiry(\n config.sessionCookie?.expiryTime,\n );\n\n const sessionToken: string = await SessionManager.createSessionToken(\n response.assertion,\n userIdFromToken,\n sessionId,\n scopes,\n expiresIn,\n '',\n organizationId,\n );\n\n setChunkedCookie(\n cookieStore,\n SessionManager.getSessionCookieName(),\n sessionToken,\n SessionManager.getSessionCookieOptions(sessionCookieExpiryTime),\n );\n\n cookieStore.delete(SessionManager.getTempSessionCookieName());\n\n const afterSignInUrl: string = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl');\n return {data: {afterSignInUrl: String(afterSignInUrl)}, success: true};\n }\n\n return {data: response as Record<string, unknown>, success: true};\n } catch (error) {\n const message =\n error instanceof Error\n ? error.message\n : typeof (error as any)?.message === 'string'\n ? (error as any).message\n : String(error);\n logger.error(`[signInAction] Error during sign-in: ${message}`);\n return {error: message, success: false};\n }\n};\n\nexport default signInAction;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAM,eAAe,OACnB,SACA,YAUI;AACJ,KAAI;EACF,MAAM,SAASA,2BAAW;EAC1B,MAAMC,cAA8B,iCAAe;EAEnD,IAAIC;EAEJ,MAAMC,uBAA2CC,uCAC/C,aACAC,+BAAe,sBAAsB,CACtC;AAED,MAAI,qBACF,KAAI;AAEF,gBAD4C,MAAMA,+BAAe,mBAAmB,qBAAqB,EAC9E;UACrB;AAKV,MAAI,CAAC,WAAW;GACd,MAAMC,mBAAuC,YAAY,IAAID,+BAAe,0BAA0B,CAAC,EAAE;AAEzG,OAAI,iBACF,KAAI;AAEF,iBADyC,MAAMA,+BAAe,kBAAkB,iBAAiB,EACzE;WAClB;;AAMZ,MAAI,CAAC,WAAW;AACd,wDAA+B;GAE/B,MAAME,mBAA2B,MAAMF,+BAAe,kBAAkB,UAAU;AAElF,eAAY,IACVA,+BAAe,0BAA0B,EACzC,kBACAA,+BAAe,6BAA6B,CAC7C;;AAIH,MAAI,CAAC,yCAAmB,QAAQ,EAAE;GAChC,MAAMG,mBAA2B,MAAM,OAAO,uBAAuB,EAAE,EAAE,UAAU;AACnF,UAAO;IAAC,MAAM,EAAC,WAAW,OAAO,iBAAiB,EAAC;IAAE,SAAS;IAAK;;EAIrE,MAAMC,WAAgB,MAAM,OAAO,OAAO,SAAS,SAAU,UAAU;AAEvE,MAAI,SAAS,eAAeC,0CAAyB,UAAU;AAK7D,OAAI,CAAC,SAAS,UACZ,OAAM,IAAI,MAAM,sDAAsD;GAGxE,MAAMC,UAAmB,MAAM,OAAO,kBAAkB,WAAW,SAAS,UAAU;GACtF,MAAMC,kBAA2B,QAAQ,OAA8B;GACvE,MAAMC,iBAAsC,QAAQ,eAAe,QAAQ;GAG3E,MAAMC,SAAkB,QAAQ,YAAmC;GACnE,MAAMC,MAAe,QAAQ,UAAiC,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;GAC3F,MAAMC,MAAe,QAAQ,UAAiC,MAAM;GACpE,MAAMC,YAAoB,KAAK,IAAI,MAAM,KAAK,EAAE;GAEhD,MAAMC,SAA8B,MAAM,OAAO,kBAAkB;GACnE,MAAMC,0BAAkCd,+BAAe,2BACrD,OAAO,eAAe,WACvB;GAED,MAAMe,eAAuB,MAAMf,+BAAe,mBAChD,SAAS,WACT,iBACA,WACA,QACA,WACA,IACA,eACD;AAED,0CACE,aACAA,+BAAe,sBAAsB,EACrC,cACAA,+BAAe,wBAAwB,wBAAwB,CAChE;AAED,eAAY,OAAOA,+BAAe,0BAA0B,CAAC;GAE7D,MAAMgB,iBAAyB,OAAO,MAAM,OAAO,mBAAmB,EAAE,uBAAuB,iBAAiB;AAChH,UAAO;IAAC,MAAM,EAAC,gBAAgB,OAAO,eAAe,EAAC;IAAE,SAAS;IAAK;;AAGxE,SAAO;GAAC,MAAM;GAAqC,SAAS;GAAK;UAC1D,OAAO;EACd,MAAM,UACJ,iBAAiB,QACb,MAAM,UACN,OAAQ,OAAe,YAAY,WAChC,MAAc,UACf,OAAO,MAAM;AACrB,yBAAO,MAAM,wCAAwC,UAAU;AAC/D,SAAO;GAAC,OAAO;GAAS,SAAS;GAAM;;;AAI3C,2BAAe"}
1
+ {"version":3,"file":"signInAction.cjs","names":["getClient","cookieStore: RequestCookies","sessionId: string | undefined","existingSessionToken: string | undefined","getChunkedCookie","SessionManager","tempSessionToken: string | undefined","tempSessionToken: string","defaultSignInUrl: string","response: any","EmbeddedSignInFlowStatus","idToken: IdToken","userIdFromToken: string","organizationId: string | undefined","scopes: string","iat: number","exp: number","expiresIn: number","config: ThunderIDNextConfig","sessionCookieExpiryTime: number","sessionToken: string","afterSignInUrl: string"],"sources":["../../../../src/server/actions/signInAction.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\n'use server';\n\nimport {\n generateSessionId,\n EmbeddedSignInFlowStatus,\n EmbeddedFlowExecuteRequestConfig,\n IdToken,\n isEmpty,\n} from '@thunderid/node';\nimport {cookies} from 'next/headers';\nimport {ThunderIDNextConfig} from '../../models/config';\nimport {getChunkedCookie, setChunkedCookie} from '../../utils/chunkedCookie';\nimport logger from '../../utils/logger';\nimport SessionManager, {SessionTokenPayload} from '../../utils/SessionManager';\nimport getClient from '../getClient';\n\ntype RequestCookies = Awaited<ReturnType<typeof cookies>>;\n\n/**\n * Server action for signing in a user.\n * Handles the embedded sign-in flow and manages session cookies.\n *\n * @param payload - The embedded sign-in flow payload\n * @param request - The embedded flow execute request config\n * @returns Promise that resolves when sign-in is complete\n */\nconst signInAction = async (\n payload?: any,\n request?: EmbeddedFlowExecuteRequestConfig,\n): Promise<{\n data?:\n | {\n afterSignInUrl?: string;\n signInUrl?: string;\n }\n | Record<string, unknown>;\n error?: string;\n success: boolean;\n}> => {\n try {\n const client = getClient();\n const cookieStore: RequestCookies = await cookies();\n\n let sessionId: string | undefined;\n\n const existingSessionToken: string | undefined = getChunkedCookie(\n cookieStore,\n SessionManager.getSessionCookieName(),\n );\n\n if (existingSessionToken) {\n try {\n const sessionPayload: SessionTokenPayload = await SessionManager.verifySessionToken(existingSessionToken);\n sessionId = sessionPayload.sessionId;\n } catch {\n // Invalid session token, will create new temp session\n }\n }\n\n if (!sessionId) {\n const tempSessionToken: string | undefined = cookieStore.get(SessionManager.getTempSessionCookieName())?.value;\n\n if (tempSessionToken) {\n try {\n const tempSession: {sessionId: string} = await SessionManager.verifyTempSession(tempSessionToken);\n sessionId = tempSession.sessionId;\n } catch {\n // Invalid temp session, will create new one\n }\n }\n }\n\n if (!sessionId) {\n sessionId = generateSessionId();\n\n const tempSessionToken: string = await SessionManager.createTempSession(sessionId);\n\n cookieStore.set(\n SessionManager.getTempSessionCookieName(),\n tempSessionToken,\n SessionManager.getTempSessionCookieOptions(),\n );\n }\n\n // If no payload provided, redirect to sign-in URL for redirect-based sign-in.\n if (!payload || isEmpty(payload)) {\n const defaultSignInUrl: string = await client.getAuthorizeRequestUrl({}, sessionId);\n return {data: {signInUrl: String(defaultSignInUrl)}, success: true};\n }\n\n // Handle embedded sign-in flow\n const response: any = await client.signIn(payload, request!, sessionId);\n\n if (response.flowStatus === EmbeddedSignInFlowStatus.Complete) {\n // On the V2 platform, a completed embedded flow (without a preceding `/authorize` redirect,\n // i.e. no `authId`) returns a self-contained JWT `assertion` rather than an authorization\n // code to exchange. This mirrors how @thunderid/react and @thunderid/vue treat it: the\n // assertion is used directly as the bearer session token.\n if (!response.assertion) {\n throw new Error('[signInAction] Flow completed without an assertion.');\n }\n\n const idToken: IdToken = await client.getDecodedIdToken(sessionId, response.assertion);\n const userIdFromToken: string = (idToken.sub as string | undefined) ?? sessionId;\n const organizationId: string | undefined = (idToken['user_org'] || idToken['organization_id']) as\n | string\n | undefined;\n const scopes: string = (idToken['scope'] as string | undefined) ?? '';\n const iat: number = (idToken['iat'] as number | undefined) ?? Math.floor(Date.now() / 1000);\n const exp: number = (idToken['exp'] as number | undefined) ?? iat + 3600;\n const expiresIn: number = Math.max(exp - iat, 0);\n\n const config: ThunderIDNextConfig = await client.getConfiguration();\n const sessionCookieExpiryTime: number = SessionManager.resolveSessionCookieExpiry(\n config.sessionCookie?.expiryTime,\n );\n\n const sessionToken: string = await SessionManager.createSessionToken(\n response.assertion,\n userIdFromToken,\n sessionId,\n scopes,\n expiresIn,\n '',\n organizationId,\n {sid: typeof idToken['sid'] === 'string' ? idToken['sid'] : undefined},\n );\n\n setChunkedCookie(\n cookieStore,\n SessionManager.getSessionCookieName(),\n sessionToken,\n SessionManager.getSessionCookieOptions(sessionCookieExpiryTime),\n );\n\n cookieStore.delete(SessionManager.getTempSessionCookieName());\n\n const afterSignInUrl: string = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl');\n return {data: {afterSignInUrl: String(afterSignInUrl)}, success: true};\n }\n\n return {data: response as Record<string, unknown>, success: true};\n } catch (error) {\n const message =\n error instanceof Error\n ? error.message\n : typeof (error as any)?.message === 'string'\n ? (error as any).message\n : String(error);\n logger.error(`[signInAction] Error during sign-in: ${message}`);\n return {error: message, success: false};\n }\n};\n\nexport default signInAction;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAM,eAAe,OACnB,SACA,YAUI;AACJ,KAAI;EACF,MAAM,SAASA,2BAAW;EAC1B,MAAMC,cAA8B,iCAAe;EAEnD,IAAIC;EAEJ,MAAMC,uBAA2CC,uCAC/C,aACAC,+BAAe,sBAAsB,CACtC;AAED,MAAI,qBACF,KAAI;AAEF,gBAD4C,MAAMA,+BAAe,mBAAmB,qBAAqB,EAC9E;UACrB;AAKV,MAAI,CAAC,WAAW;GACd,MAAMC,mBAAuC,YAAY,IAAID,+BAAe,0BAA0B,CAAC,EAAE;AAEzG,OAAI,iBACF,KAAI;AAEF,iBADyC,MAAMA,+BAAe,kBAAkB,iBAAiB,EACzE;WAClB;;AAMZ,MAAI,CAAC,WAAW;AACd,wDAA+B;GAE/B,MAAME,mBAA2B,MAAMF,+BAAe,kBAAkB,UAAU;AAElF,eAAY,IACVA,+BAAe,0BAA0B,EACzC,kBACAA,+BAAe,6BAA6B,CAC7C;;AAIH,MAAI,CAAC,yCAAmB,QAAQ,EAAE;GAChC,MAAMG,mBAA2B,MAAM,OAAO,uBAAuB,EAAE,EAAE,UAAU;AACnF,UAAO;IAAC,MAAM,EAAC,WAAW,OAAO,iBAAiB,EAAC;IAAE,SAAS;IAAK;;EAIrE,MAAMC,WAAgB,MAAM,OAAO,OAAO,SAAS,SAAU,UAAU;AAEvE,MAAI,SAAS,eAAeC,0CAAyB,UAAU;AAK7D,OAAI,CAAC,SAAS,UACZ,OAAM,IAAI,MAAM,sDAAsD;GAGxE,MAAMC,UAAmB,MAAM,OAAO,kBAAkB,WAAW,SAAS,UAAU;GACtF,MAAMC,kBAA2B,QAAQ,OAA8B;GACvE,MAAMC,iBAAsC,QAAQ,eAAe,QAAQ;GAG3E,MAAMC,SAAkB,QAAQ,YAAmC;GACnE,MAAMC,MAAe,QAAQ,UAAiC,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;GAC3F,MAAMC,MAAe,QAAQ,UAAiC,MAAM;GACpE,MAAMC,YAAoB,KAAK,IAAI,MAAM,KAAK,EAAE;GAEhD,MAAMC,SAA8B,MAAM,OAAO,kBAAkB;GACnE,MAAMC,0BAAkCd,+BAAe,2BACrD,OAAO,eAAe,WACvB;GAED,MAAMe,eAAuB,MAAMf,+BAAe,mBAChD,SAAS,WACT,iBACA,WACA,QACA,WACA,IACA,gBACA,EAAC,KAAK,OAAO,QAAQ,WAAW,WAAW,QAAQ,SAAS,QAAU,CACvE;AAED,0CACE,aACAA,+BAAe,sBAAsB,EACrC,cACAA,+BAAe,wBAAwB,wBAAwB,CAChE;AAED,eAAY,OAAOA,+BAAe,0BAA0B,CAAC;GAE7D,MAAMgB,iBAAyB,OAAO,MAAM,OAAO,mBAAmB,EAAE,uBAAuB,iBAAiB;AAChH,UAAO;IAAC,MAAM,EAAC,gBAAgB,OAAO,eAAe,EAAC;IAAE,SAAS;IAAK;;AAGxE,SAAO;GAAC,MAAM;GAAqC,SAAS;GAAK;UAC1D,OAAO;EACd,MAAM,UACJ,iBAAiB,QACb,MAAM,UACN,OAAQ,OAAe,YAAY,WAChC,MAAc,UACf,OAAO,MAAM;AACrB,yBAAO,MAAM,wCAAwC,UAAU;AAC/D,SAAO;GAAC,OAAO;GAAS,SAAS;GAAM;;;AAI3C,2BAAe"}
@@ -0,0 +1,41 @@
1
+ 'use server';
2
+
3
+
4
+ const require_rolldown_runtime = require('../../_virtual/rolldown_runtime.cjs');
5
+ const require_getClient = require('../getClient.cjs');
6
+ let __thunderid_node = require("@thunderid/node");
7
+ __thunderid_node = require_rolldown_runtime.__toESM(__thunderid_node);
8
+
9
+ //#region src/server/actions/updateUserCredentialsAction.ts
10
+ /**
11
+ * Server action to update one of the signed-in user's own credentials (e.g. `password`).
12
+ *
13
+ * Never throws across the server action boundary — a thrown class instance loses its
14
+ * prototype chain crossing it, so `error instanceof ThunderIDAPIError` would no longer hold on
15
+ * the client. Instead, the real error is mapped to a field/message here, server-side, while it
16
+ * is still the genuine `ThunderIDAPIError` the core `updateMeCredentials` call threw, and only
17
+ * the plain, serializable result crosses back.
18
+ */
19
+ const updateUserCredentialsAction = async (payload, sessionId) => {
20
+ try {
21
+ await require_getClient.default().updateUserCredentials(payload, sessionId);
22
+ return {
23
+ field: null,
24
+ messageKey: "",
25
+ success: true
26
+ };
27
+ } catch (error) {
28
+ const { field, message, messageKey } = (0, __thunderid_node.mapCredentialUpdateError)(error);
29
+ return {
30
+ field,
31
+ message,
32
+ messageKey,
33
+ success: false
34
+ };
35
+ }
36
+ };
37
+ var updateUserCredentialsAction_default = updateUserCredentialsAction;
38
+
39
+ //#endregion
40
+ exports.default = updateUserCredentialsAction_default;
41
+ //# sourceMappingURL=updateUserCredentialsAction.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updateUserCredentialsAction.cjs","names":["getClient"],"sources":["../../../../src/server/actions/updateUserCredentialsAction.ts"],"sourcesContent":["// Copyright 2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\n'use server';\n\nimport {CredentialUpdateErrorResult, mapCredentialUpdateError} from '@thunderid/node';\nimport getClient from '../getClient';\n\nexport interface UpdateUserCredentialsActionResult extends CredentialUpdateErrorResult {\n success: boolean;\n}\n\n/**\n * Server action to update one of the signed-in user's own credentials (e.g. `password`).\n *\n * Never throws across the server action boundary — a thrown class instance loses its\n * prototype chain crossing it, so `error instanceof ThunderIDAPIError` would no longer hold on\n * the client. Instead, the real error is mapped to a field/message here, server-side, while it\n * is still the genuine `ThunderIDAPIError` the core `updateMeCredentials` call threw, and only\n * the plain, serializable result crosses back.\n */\nconst updateUserCredentialsAction = async (\n payload: Record<string, string>,\n sessionId?: string,\n): Promise<UpdateUserCredentialsActionResult> => {\n try {\n const client = getClient();\n await client.updateUserCredentials(payload, sessionId);\n return {field: null, messageKey: '', success: true};\n } catch (error) {\n const {field, message, messageKey}: CredentialUpdateErrorResult = mapCredentialUpdateError(error);\n return {field, message, messageKey, success: false};\n }\n};\n\nexport default updateUserCredentialsAction;\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,MAAM,8BAA8B,OAClC,SACA,cAC+C;AAC/C,KAAI;AAEF,QADeA,2BAAW,CACb,sBAAsB,SAAS,UAAU;AACtD,SAAO;GAAC,OAAO;GAAM,YAAY;GAAI,SAAS;GAAK;UAC5C,OAAO;EACd,MAAM,EAAC,OAAO,SAAS,8DAAoE,MAAM;AACjG,SAAO;GAAC;GAAO;GAAS;GAAY,SAAS;GAAM;;;AAIvD,0CAAe"}
@@ -0,0 +1,92 @@
1
+ const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
2
+ const require_loggedOutSessions = require('../utils/loggedOutSessions.cjs');
3
+ const require_SessionManager = require('../utils/SessionManager.cjs');
4
+ const require_getClient = require('./getClient.cjs');
5
+ const require_logger = require('../utils/logger.cjs');
6
+ let __thunderid_node = require("@thunderid/node");
7
+ __thunderid_node = require_rolldown_runtime.__toESM(__thunderid_node);
8
+
9
+ //#region src/server/backchannelLogout.ts
10
+ const BODY_LIMIT_BYTES = 16 * 1024;
11
+ const respond = (status) => new Response(null, {
12
+ headers: { "Cache-Control": "no-store" },
13
+ status
14
+ });
15
+ /**
16
+ * Reads the request body as text, giving up once it passes the limit.
17
+ */
18
+ const readBody = async (request) => {
19
+ if (Number(request.headers.get("content-length") ?? 0) > BODY_LIMIT_BYTES || !request.body) return;
20
+ const reader = request.body.getReader();
21
+ const decoder = new TextDecoder();
22
+ let body = "";
23
+ let size = 0;
24
+ for (;;) {
25
+ const { done, value } = await reader.read();
26
+ if (done) return body + decoder.decode();
27
+ size += value.byteLength;
28
+ if (size > BODY_LIMIT_BYTES) {
29
+ await reader.cancel();
30
+ return;
31
+ }
32
+ body += decoder.decode(value, { stream: true });
33
+ }
34
+ };
35
+ /**
36
+ * Creates the route handler for OpenID Connect back-channel logout. ThunderID posts a
37
+ * `logout_token` to it when a session this application took part in ends. The session lives in a
38
+ * cookie the server cannot delete, so the handler records the logout, and every later request
39
+ * that presents a matching session cookie is treated as signed out.
40
+ *
41
+ * Mount it at the path registered as the application's back-channel logout URI:
42
+ *
43
+ * ```ts
44
+ * // app/api/auth/backchannel-logout/route.ts
45
+ * import {backchannelLogout} from '@thunderid/nextjs/server';
46
+ *
47
+ * export const {POST} = backchannelLogout();
48
+ * ```
49
+ *
50
+ * It answers `200` for a valid token, including one that names no session held here, `400` for a
51
+ * request or token that is not valid, and `500` when the logout could not be recorded, which
52
+ * makes the server try again. It needs no session cookie: the token's signature is the
53
+ * authentication.
54
+ */
55
+ const backchannelLogout = (options = {}) => {
56
+ require_loggedOutSessions.setLogoutRecordStore(options.store);
57
+ const POST = async (request) => {
58
+ if (!(request.headers.get("content-type")?.toLowerCase() ?? "").startsWith("application/x-www-form-urlencoded")) return respond(400);
59
+ const body = await readBody(request);
60
+ const logoutToken = body === void 0 ? null : new URLSearchParams(body).get(__thunderid_node.BackchannelLogoutConstants.TOKEN_PARAM);
61
+ if (!logoutToken) return respond(400);
62
+ try {
63
+ const client = require_getClient.default();
64
+ if (!client.isInitialized) await client.initialize({});
65
+ const result = await client.handleBackchannelLogout(logoutToken);
66
+ const config = await client.getConfiguration();
67
+ await require_loggedOutSessions.getLoggedOutSessions().record({
68
+ ...result,
69
+ clockTolerance: config?.tokenValidation?.idToken?.clockTolerance
70
+ }, require_SessionManager.default.resolveSessionCookieExpiry(config?.sessionCookie?.expiryTime));
71
+ try {
72
+ await options.onLogout?.(result);
73
+ } catch {
74
+ require_logger.default.error("[backchannelLogout] The onLogout hook failed.");
75
+ }
76
+ return respond(200);
77
+ } catch (error) {
78
+ if (error instanceof __thunderid_node.InvalidLogoutTokenError) {
79
+ require_logger.default.warn(`[backchannelLogout] Rejected a logout token (${error.code}).`);
80
+ return respond(400);
81
+ }
82
+ require_logger.default.error("[backchannelLogout] Could not complete a back-channel logout.");
83
+ return respond(500);
84
+ }
85
+ };
86
+ return { POST };
87
+ };
88
+ var backchannelLogout_default = backchannelLogout;
89
+
90
+ //#endregion
91
+ exports.default = backchannelLogout_default;
92
+ //# sourceMappingURL=backchannelLogout.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backchannelLogout.cjs","names":["reader: ReadableStreamDefaultReader<Uint8Array>","decoder: TextDecoder","body: string | undefined","logoutToken: string | null","BackchannelLogoutConstants","client: ThunderIDNextClient","getClient","result: BackchannelLogoutResult","config: ThunderIDNextConfig","getLoggedOutSessions","SessionManager","error: unknown","InvalidLogoutTokenError"],"sources":["../../../src/server/backchannelLogout.ts"],"sourcesContent":["// Copyright 2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {\n BackchannelLogoutConstants,\n BackchannelLogoutResult,\n InvalidLogoutTokenError,\n LogoutRecordStore,\n} from '@thunderid/node';\nimport getClient from './getClient';\nimport {ThunderIDNextConfig} from '../models/config';\nimport ThunderIDNextClient from '../ThunderIDNextClient';\nimport {getLoggedOutSessions, setLogoutRecordStore} from '../utils/loggedOutSessions';\nimport logger from '../utils/logger';\nimport SessionManager from '../utils/SessionManager';\n\n// A logout token is a kilobyte or two. The endpoint is unauthenticated, so nothing larger is read.\nconst BODY_LIMIT_BYTES = 16 * 1024;\n\nexport interface BackchannelLogoutOptions {\n /**\n * Called after a logout was recorded, so the application can clear what it holds for the ended\n * sessions. Best effort: a failure is logged and the handler still answers 200, since the logout\n * is already recorded and a retry would find nothing new.\n */\n onLogout?: (result: BackchannelLogoutResult) => void | Promise<void>;\n /**\n * Where ended sessions are recorded. Give the same store to `thunderIDProxy()`. Needed when more\n * than one instance of the application runs, or when the proxy runs on the Edge runtime, which\n * shares no memory with this handler. Defaults to the memory of this process.\n */\n store?: LogoutRecordStore;\n}\n\nconst respond = (status: number): Response => new Response(null, {headers: {'Cache-Control': 'no-store'}, status});\n\n/**\n * Reads the request body as text, giving up once it passes the limit.\n */\nconst readBody = async (request: Request): Promise<string | undefined> => {\n if (Number(request.headers.get('content-length') ?? 0) > BODY_LIMIT_BYTES || !request.body) {\n return undefined;\n }\n\n const reader: ReadableStreamDefaultReader<Uint8Array> = request.body.getReader();\n const decoder: TextDecoder = new TextDecoder();\n let body = '';\n let size = 0;\n\n for (;;) {\n const {done, value} = await reader.read();\n\n if (done) {\n return body + decoder.decode();\n }\n\n size += value.byteLength;\n\n if (size > BODY_LIMIT_BYTES) {\n await reader.cancel();\n\n return undefined;\n }\n\n body += decoder.decode(value, {stream: true});\n }\n};\n\n/**\n * Creates the route handler for OpenID Connect back-channel logout. ThunderID posts a\n * `logout_token` to it when a session this application took part in ends. The session lives in a\n * cookie the server cannot delete, so the handler records the logout, and every later request\n * that presents a matching session cookie is treated as signed out.\n *\n * Mount it at the path registered as the application's back-channel logout URI:\n *\n * ```ts\n * // app/api/auth/backchannel-logout/route.ts\n * import {backchannelLogout} from '@thunderid/nextjs/server';\n *\n * export const {POST} = backchannelLogout();\n * ```\n *\n * It answers `200` for a valid token, including one that names no session held here, `400` for a\n * request or token that is not valid, and `500` when the logout could not be recorded, which\n * makes the server try again. It needs no session cookie: the token's signature is the\n * authentication.\n */\nconst backchannelLogout = (options: BackchannelLogoutOptions = {}): {POST: (request: Request) => Promise<Response>} => {\n setLogoutRecordStore(options.store);\n\n const POST = async (request: Request): Promise<Response> => {\n const contentType: string = request.headers.get('content-type')?.toLowerCase() ?? '';\n\n if (!contentType.startsWith('application/x-www-form-urlencoded')) {\n return respond(400);\n }\n\n const body: string | undefined = await readBody(request);\n const logoutToken: string | null =\n body === undefined ? null : new URLSearchParams(body).get(BackchannelLogoutConstants.TOKEN_PARAM);\n\n if (!logoutToken) {\n return respond(400);\n }\n\n try {\n const client: ThunderIDNextClient = getClient();\n\n // A logout request can be the first thing a server process handles.\n if (!client.isInitialized) {\n await client.initialize({} as ThunderIDNextConfig);\n }\n\n const result: BackchannelLogoutResult = await client.handleBackchannelLogout(logoutToken);\n // Typed as synchronous, but it resolves from storage.\n const config: ThunderIDNextConfig = await (client.getConfiguration() as unknown as Promise<ThunderIDNextConfig>);\n\n // A session cookie cannot outlive this, so neither does the record that ends it.\n await getLoggedOutSessions().record(\n {...result, clockTolerance: config?.tokenValidation?.idToken?.clockTolerance},\n SessionManager.resolveSessionCookieExpiry(config?.sessionCookie?.expiryTime),\n );\n try {\n await options.onLogout?.(result);\n } catch {\n logger.error('[backchannelLogout] The onLogout hook failed.');\n }\n\n return respond(200);\n } catch (error: unknown) {\n if (error instanceof InvalidLogoutTokenError) {\n // The code says which check failed. The token itself is never logged.\n logger.warn(`[backchannelLogout] Rejected a logout token (${error.code}).`);\n\n return respond(400);\n }\n\n logger.error('[backchannelLogout] Could not complete a back-channel logout.');\n\n return respond(500);\n }\n };\n\n return {POST};\n};\n\nexport default backchannelLogout;\n"],"mappings":";;;;;;;;;AAiBA,MAAM,mBAAmB,KAAK;AAiB9B,MAAM,WAAW,WAA6B,IAAI,SAAS,MAAM;CAAC,SAAS,EAAC,iBAAiB,YAAW;CAAE;CAAO,CAAC;;;;AAKlH,MAAM,WAAW,OAAO,YAAkD;AACxE,KAAI,OAAO,QAAQ,QAAQ,IAAI,iBAAiB,IAAI,EAAE,GAAG,oBAAoB,CAAC,QAAQ,KACpF;CAGF,MAAMA,SAAkD,QAAQ,KAAK,WAAW;CAChF,MAAMC,UAAuB,IAAI,aAAa;CAC9C,IAAI,OAAO;CACX,IAAI,OAAO;AAEX,UAAS;EACP,MAAM,EAAC,MAAM,UAAS,MAAM,OAAO,MAAM;AAEzC,MAAI,KACF,QAAO,OAAO,QAAQ,QAAQ;AAGhC,UAAQ,MAAM;AAEd,MAAI,OAAO,kBAAkB;AAC3B,SAAM,OAAO,QAAQ;AAErB;;AAGF,UAAQ,QAAQ,OAAO,OAAO,EAAC,QAAQ,MAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;AAwBjD,MAAM,qBAAqB,UAAoC,EAAE,KAAsD;AACrH,gDAAqB,QAAQ,MAAM;CAEnC,MAAM,OAAO,OAAO,YAAwC;AAG1D,MAAI,EAFwB,QAAQ,QAAQ,IAAI,eAAe,EAAE,aAAa,IAAI,IAEjE,WAAW,oCAAoC,CAC9D,QAAO,QAAQ,IAAI;EAGrB,MAAMC,OAA2B,MAAM,SAAS,QAAQ;EACxD,MAAMC,cACJ,SAAS,SAAY,OAAO,IAAI,gBAAgB,KAAK,CAAC,IAAIC,4CAA2B,YAAY;AAEnG,MAAI,CAAC,YACH,QAAO,QAAQ,IAAI;AAGrB,MAAI;GACF,MAAMC,SAA8BC,2BAAW;AAG/C,OAAI,CAAC,OAAO,cACV,OAAM,OAAO,WAAW,EAAE,CAAwB;GAGpD,MAAMC,SAAkC,MAAM,OAAO,wBAAwB,YAAY;GAEzF,MAAMC,SAA8B,MAAO,OAAO,kBAAkB;AAGpE,SAAMC,gDAAsB,CAAC,OAC3B;IAAC,GAAG;IAAQ,gBAAgB,QAAQ,iBAAiB,SAAS;IAAe,EAC7EC,+BAAe,2BAA2B,QAAQ,eAAe,WAAW,CAC7E;AACD,OAAI;AACF,UAAM,QAAQ,WAAW,OAAO;WAC1B;AACN,2BAAO,MAAM,gDAAgD;;AAG/D,UAAO,QAAQ,IAAI;WACZC,OAAgB;AACvB,OAAI,iBAAiBC,0CAAyB;AAE5C,2BAAO,KAAK,gDAAgD,MAAM,KAAK,IAAI;AAE3E,WAAO,QAAQ,IAAI;;AAGrB,0BAAO,MAAM,gEAAgE;AAE7E,UAAO,QAAQ,IAAI;;;AAIvB,QAAO,EAAC,MAAK;;AAGf,gCAAe"}
@@ -2,8 +2,10 @@ const require_thunderid = require('./thunderid.cjs');
2
2
  const require_ThunderIDProvider = require('./ThunderIDProvider.cjs');
3
3
  const require_thunderIDProxy = require('./proxy/thunderIDProxy.cjs');
4
4
  const require_createRouteMatcher = require('./proxy/createRouteMatcher.cjs');
5
+ const require_backchannelLogout = require('./backchannelLogout.cjs');
5
6
 
6
7
  exports.ThunderIDProvider = require_ThunderIDProvider.default;
8
+ exports.backchannelLogout = require_backchannelLogout.default;
7
9
  exports.createRouteMatcher = require_createRouteMatcher.default;
8
10
  exports.removeChunkedCookieFromHeader = require_thunderIDProxy.removeChunkedCookieFromHeader;
9
11
  exports.replaceChunkedCookieInHeader = require_thunderIDProxy.replaceChunkedCookieInHeader;
@@ -1,5 +1,6 @@
1
1
  const require_rolldown_runtime = require('../../_virtual/rolldown_runtime.cjs');
2
2
  const require_chunkedCookie = require('../../utils/chunkedCookie.cjs');
3
+ const require_loggedOutSessions = require('../../utils/loggedOutSessions.cjs');
3
4
  const require_sessionConstants = require('../../constants/sessionConstants.cjs');
4
5
  const require_SessionManager = require('../../utils/SessionManager.cjs');
5
6
  const require_decorateConfigWithNextEnv = require('../../utils/decorateConfigWithNextEnv.cjs');
@@ -106,7 +107,9 @@ const toChunkedCookieWriter = (request, responseCookies) => ({
106
107
  * ```
107
108
  */
108
109
  const thunderIDProxy = (handler, options) => async (request) => {
109
- const resolvedConfig = require_decorateConfigWithNextEnv.default(typeof options === "function" ? options(request) : options || {});
110
+ const resolvedOptions = typeof options === "function" ? options(request) : options || {};
111
+ require_loggedOutSessions.setLogoutRecordStore(resolvedOptions.backchannelLogout?.store);
112
+ const resolvedConfig = require_decorateConfigWithNextEnv.default(resolvedOptions);
110
113
  const url = new URL(request.url);
111
114
  const hasCallbackParams = url.searchParams.has("code") && url.searchParams.has("state");
112
115
  let isValidOAuthCallback = false;
@@ -1 +1 @@
1
- {"version":3,"file":"thunderIDProxy.cjs","names":["eqIdx: number","entries: {name: string; part: string}[]","CookieChunking","remaining: string[]","newParts: string[]","resolvedConfig: ThunderIDNextConfig","decorateConfigWithNextEnv","url: URL","hasCallbackParams: boolean","tempSessionToken: string | undefined","SessionManager","verifiedSession: SessionTokenPayload | undefined","getSessionFromRequest","expiredSession: SessionTokenPayload | undefined","rawToken: string | undefined","getChunkedCookie","decoded: SessionTokenPayload","now: number","candidateSession: SessionTokenPayload | undefined","needsRefresh: boolean","REFRESH_BUFFER_SECONDS","activeSession: SessionTokenPayload | undefined","refreshCookieUpdate: {expiry: number; token: string} | undefined","handleRefreshToken","rawSessionCookie: string | undefined","sessionId: string | undefined","getSessionIdFromRequest","handlerResponse: NextResponse | void","referer: string | null","refererUrl: URL","requestUrl: URL","redirectUrl: string","NextResponse","cookieName: string","cookieName","requestHeaders: Headers","cleanResponse: NextResponse","requestHeaders","cookieOptions: ReturnType<typeof SessionManager.getSessionCookieOptions>","updatedCookieHeader: string","response: NextResponse"],"sources":["../../../../src/server/proxy/thunderIDProxy.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {CookieChunking} from '@thunderid/node';\nimport {NextRequest, NextResponse} from 'next/server';\nimport {REFRESH_BUFFER_SECONDS} from '../../constants/sessionConstants';\nimport {ThunderIDNextConfig} from '../../models/config';\nimport {\n ChunkedCookieOptions,\n ChunkedCookieWriter,\n deleteChunkedCookie,\n getChunkedCookie,\n setChunkedCookie,\n} from '../../utils/chunkedCookie';\nimport decorateConfigWithNextEnv from '../../utils/decorateConfigWithNextEnv';\nimport handleRefreshToken from '../../utils/handleRefreshToken';\nimport SessionManager, {SessionTokenPayload} from '../../utils/SessionManager';\nimport {getSessionFromRequest, getSessionIdFromRequest} from '../../utils/sessionUtils';\n\nexport type ThunderIDProxyOptions = Partial<ThunderIDNextConfig>;\n\nexport interface ThunderIDProxyContext {\n /** Get the session payload from JWT session if available */\n getSession: () => Promise<SessionTokenPayload | undefined>;\n /** Get the session ID from the current request */\n getSessionId: () => string | undefined;\n /** Check if the current request has a valid ThunderID session */\n isSignedIn: () => boolean;\n /**\n * Protect a route by redirecting unauthenticated users.\n * Redirect URL fallback order:\n * 1. options.redirect\n * 2. resolvedOptions.signInUrl\n * 3. resolvedOptions.defaultRedirect\n * 4. referer (if from same origin)\n * If none are available, falls back to '/'.\n */\n protectRoute: (routeOptions?: {redirect?: string}) => Promise<NextResponse | void>;\n}\n\ntype ThunderIDProxyHandler = (\n thunderid: ThunderIDProxyContext,\n req: NextRequest,\n) => Promise<NextResponse | void> | NextResponse | void;\n\n/**\n * Splits a raw Cookie header string into its `{name, part}` entries, where\n * `part` is the original `name=value` text (trimmed, never empty).\n */\nconst parseCookieHeaderParts = (cookieHeader: string): {name: string; part: string}[] =>\n cookieHeader\n .split(';')\n .map((p: string) => p.trim())\n .filter(Boolean)\n .map((part: string) => {\n const eqIdx: number = part.indexOf('=');\n const name: string = eqIdx === -1 ? part : part.slice(0, eqIdx).trim();\n return {name, part};\n });\n\n/**\n * Removes a cookie from a raw Cookie header string — the unchunked `name`\n * entry and/or any numbered `${name}.0`, `${name}.1`, ... chunk entries.\n */\nexport const removeChunkedCookieFromHeader = (cookieHeader: string, name: string): string => {\n const entries: {name: string; part: string}[] = parseCookieHeaderParts(cookieHeader);\n const toRemove = new Set<string>(\n CookieChunking.filterChunkNames(\n name,\n entries.map((e) => e.name),\n ),\n );\n\n return entries\n .filter((e) => !toRemove.has(e.name))\n .map((e) => e.part)\n .join('; ');\n};\n\n/**\n * Replaces a cookie's value inside a raw Cookie header string, splitting it\n * across numbered `${name}.0`, `${name}.1`, ... chunk entries once it would\n * exceed the ~4KB per-cookie limit browsers enforce. Any entry the cookie\n * doesn't appear in yet is appended; stale chunk entries a shrunk value no\n * longer needs are dropped.\n */\nexport const replaceChunkedCookieInHeader = (cookieHeader: string, name: string, value: string): string => {\n const remaining: string[] = parseCookieHeaderParts(removeChunkedCookieFromHeader(cookieHeader, name)).map(\n (e) => e.part,\n );\n const newParts: string[] = Object.entries(CookieChunking.split(name, value)).map(\n ([chunkName, chunkValue]: [string, string]) => `${chunkName}=${chunkValue}`,\n );\n\n return [...remaining, ...newParts].join('; ');\n};\n\n/**\n * Adapts a request/response pair into a {@link ChunkedCookieWriter}: existing\n * chunk names are discovered from the incoming request's cookies (what the\n * browser currently holds), while writes/deletes apply to the response.\n */\nconst toChunkedCookieWriter = (\n request: NextRequest,\n responseCookies: NextResponse['cookies'],\n): ChunkedCookieWriter => ({\n delete: (name: string): void => {\n responseCookies.delete(name);\n },\n getAll: (): {name: string}[] => request.cookies.getAll(),\n set: (name: string, value: string, options: ChunkedCookieOptions): void => {\n responseCookies.set(name, value, options);\n },\n});\n\n/**\n * ThunderID proxy that integrates authentication into your Next.js application.\n * Similar to Clerk's clerkMiddleware pattern.\n *\n * Proactively refreshes the access token when it is within REFRESH_BUFFER_SECONDS of\n * expiry so that Server Components always receive a fresh session. The refresh also\n * recovers expired tokens as long as a refresh token is present.\n *\n * The updated session cookie is written to:\n * - The response → browser stores the new cookie for subsequent requests.\n * - The forwarded request headers → the same-request Server Component render sees\n * the fresh token immediately without waiting for the next navigation.\n *\n * Token refresh requires baseUrl, clientId, and clientSecret. These are resolved from\n * the options argument first, then from the standard ThunderID environment variables\n * (NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_CLIENT_ID,\n * THUNDERID_CLIENT_SECRET). If none are available the refresh step is skipped silently.\n *\n * @param handler - Optional handler function to customize proxy behavior\n * @param options - Configuration options for the proxy\n * @returns Next.js middleware function\n *\n * @example\n * ```typescript\n * // middleware.ts - Basic usage (config read from env vars automatically)\n * import { thunderIDProxy } from '@thunderid/nextjs/server';\n *\n * export default thunderIDProxy();\n *\n * export const config = {\n * matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],\n * };\n * ```\n *\n * @example\n * ```typescript\n * // With route protection\n * import { thunderIDProxy, createRouteMatcher } from '@thunderid/nextjs/server';\n *\n * const isProtectedRoute = createRouteMatcher(['/dashboard(.*)']);\n *\n * export default thunderIDProxy(async (thunderid, req) => {\n * if (isProtectedRoute(req)) {\n * await thunderid.protectRoute();\n * }\n * });\n * ```\n */\nconst thunderIDProxy =\n (\n handler?: ThunderIDProxyHandler,\n options?: ThunderIDProxyOptions | ((req: NextRequest) => ThunderIDProxyOptions),\n ): ((request: NextRequest) => Promise<NextResponse>) =>\n async (request: NextRequest): Promise<NextResponse> => {\n const resolvedOptions: ThunderIDProxyOptions = typeof options === 'function' ? options(request) : options || {};\n\n // Resolve full config from passed options + environment variable fallbacks.\n const resolvedConfig: ThunderIDNextConfig = decorateConfigWithNextEnv(resolvedOptions as ThunderIDNextConfig);\n\n // ── OAuth callback detection ──────────────────────────────────────────────\n const url: URL = new URL(request.url);\n const hasCallbackParams: boolean = url.searchParams.has('code') && url.searchParams.has('state');\n\n let isValidOAuthCallback = false;\n if (hasCallbackParams && !url.searchParams.has('error')) {\n const tempSessionToken: string | undefined = request.cookies.get(\n SessionManager.getTempSessionCookieName(),\n )?.value;\n if (tempSessionToken) {\n try {\n await SessionManager.verifyTempSession(tempSessionToken);\n isValidOAuthCallback = true;\n } catch {\n isValidOAuthCallback = false;\n }\n }\n }\n\n // ── Session resolution ────────────────────────────────────────────────────\n // Step 1: Attempt to get a fully verified (signature + expiry) session.\n const verifiedSession: SessionTokenPayload | undefined = await getSessionFromRequest(request);\n\n // Step 2: If no verified session exists, verify the raw cookie's signature\n // without enforcing expiry. This allows the proxy to recover from an\n // expired access token as long as the JWT is authentic and a refresh token\n // is present. Skipping the signature check here would let a tampered cookie\n // drive identity-confusion attacks since handleRefreshToken reuses `sub`,\n // `sessionId`, and `organizationId` from the input payload when minting the\n // new session JWT.\n let expiredSession: SessionTokenPayload | undefined;\n if (!verifiedSession) {\n const rawToken: string | undefined = getChunkedCookie(request.cookies, SessionManager.getSessionCookieName());\n if (rawToken) {\n try {\n const decoded: SessionTokenPayload = await SessionManager.verifySessionTokenForRefresh(rawToken);\n if (decoded.refreshToken) {\n expiredSession = decoded;\n }\n } catch {\n // Forged, tampered, wrong type, or malformed — ignore.\n }\n }\n }\n\n // ── Token refresh ─────────────────────────────────────────────────────────\n const now: number = Math.floor(Date.now() / 1000);\n const candidateSession: SessionTokenPayload | undefined = verifiedSession ?? expiredSession;\n\n // Config is required to call the token endpoint.\n const hasRefreshConfig = !!(resolvedConfig.baseUrl && resolvedConfig.clientId && resolvedConfig.clientSecret);\n\n // Refresh when:\n // a) Token is verified but within the proactive buffer window, OR\n // b) Token has already expired but a refresh token is available.\n const needsRefresh: boolean =\n !isValidOAuthCallback &&\n hasRefreshConfig &&\n !!candidateSession?.refreshToken &&\n ((!!verifiedSession && verifiedSession.exp <= now + REFRESH_BUFFER_SECONDS) || !!expiredSession);\n\n let activeSession: SessionTokenPayload | undefined = verifiedSession;\n let refreshCookieUpdate: {expiry: number; token: string} | undefined;\n\n if (needsRefresh && candidateSession) {\n try {\n const {newSessionToken, sessionCookieExpiryTime} = await handleRefreshToken(candidateSession, {\n baseUrl: resolvedConfig.baseUrl!,\n clientId: resolvedConfig.clientId!,\n clientSecret: resolvedConfig.clientSecret!,\n sessionCookie: resolvedConfig.sessionCookie,\n });\n // Verify the newly minted token so activeSession reflects fresh claims.\n activeSession = await SessionManager.verifySessionToken(newSessionToken);\n refreshCookieUpdate = {expiry: sessionCookieExpiryTime, token: newSessionToken};\n } catch {\n // Refresh failed — clear the irrecoverable session.\n activeSession = undefined;\n }\n }\n\n // ── Session cleanup detection ─────────────────────────────────────────────\n // Mark stale cookies for deletion when the session is irrecoverable. Skipped\n // during OAuth callbacks where a session cookie may not exist yet.\n const rawSessionCookie: string | undefined = getChunkedCookie(\n request.cookies,\n SessionManager.getSessionCookieName(),\n );\n\n let shouldClearCookie = false;\n\n if (!isValidOAuthCallback && rawSessionCookie && !activeSession && !refreshCookieUpdate) {\n // A cookie was present but all resolution paths (verify, decode, refresh)\n // failed — the session is dead and cannot be recovered.\n shouldClearCookie = true;\n }\n\n const sessionId: string | undefined = activeSession?.sessionId ?? (await getSessionIdFromRequest(request));\n const isAuthenticated = !!activeSession;\n\n // ── Proxy context ─────────────────────────────────────────────────────────\n const thunderid: ThunderIDProxyContext = {\n getSession: async (): Promise<SessionTokenPayload | undefined> => activeSession,\n getSessionId: (): string | undefined => sessionId,\n isSignedIn: (): boolean => isAuthenticated,\n protectRoute: async (routeOptions?: {redirect?: string}): Promise<NextResponse | void> => {\n // Skip during a valid OAuth callback to avoid redirecting before the\n // callback action has had a chance to complete.\n if (isValidOAuthCallback) {\n return undefined;\n }\n\n if (!isAuthenticated) {\n const referer: string | null = request.headers.get('referer');\n let fallbackRedirect = '/';\n\n if (referer) {\n try {\n const refererUrl: URL = new URL(referer);\n const requestUrl: URL = new URL(request.url);\n if (refererUrl.origin === requestUrl.origin) {\n fallbackRedirect = refererUrl.pathname + refererUrl.search;\n }\n } catch {\n // Invalid referer — ignore.\n }\n }\n\n const redirectUrl: string = routeOptions?.redirect ?? resolvedConfig.signInUrl! ?? fallbackRedirect;\n\n return NextResponse.redirect(new URL(redirectUrl, request.url));\n }\n\n return undefined;\n },\n };\n\n // ── Handler ───────────────────────────────────────────────────────────────\n const handlerResponse: NextResponse | void = handler ? await handler(thunderid, request) : undefined;\n\n // ── Build final response ──────────────────────────────────────────────────\n if (shouldClearCookie) {\n const cookieName: string = SessionManager.getSessionCookieName();\n\n if (handlerResponse) {\n // Handler returned a response (e.g. a redirect from protectRoute).\n // Attach the deletion so the browser discards the stale cookie (and\n // every chunk of it).\n deleteChunkedCookie(toChunkedCookieWriter(request, handlerResponse.cookies), cookieName);\n return handlerResponse;\n }\n\n // Pass-through: strip the dead cookie from the forwarded request headers\n // so the same-request Server Component render sees no session at all.\n const requestHeaders: Headers = new Headers(request.headers);\n requestHeaders.set('cookie', removeChunkedCookieFromHeader(request.headers.get('cookie') ?? '', cookieName));\n const cleanResponse: NextResponse = NextResponse.next({request: {headers: requestHeaders}});\n deleteChunkedCookie(toChunkedCookieWriter(request, cleanResponse.cookies), cookieName);\n return cleanResponse;\n }\n\n if (!refreshCookieUpdate) {\n return handlerResponse ?? NextResponse.next();\n }\n\n // A token refresh occurred — the new session cookie must be applied to:\n // 1. The HTTP response so the browser stores the updated cookie.\n // 2. The forwarded request headers so the same-request Server Component\n // render reads the fresh session token instead of the expired one.\n const cookieName: string = SessionManager.getSessionCookieName();\n const cookieOptions: ReturnType<typeof SessionManager.getSessionCookieOptions> =\n SessionManager.getSessionCookieOptions(refreshCookieUpdate.expiry);\n\n if (handlerResponse) {\n // Handler returned a response (e.g. a redirect from protectRoute).\n // Attach the refresh cookie so the browser receives it even on redirects.\n setChunkedCookie(\n toChunkedCookieWriter(request, handlerResponse.cookies),\n cookieName,\n refreshCookieUpdate.token,\n cookieOptions,\n );\n return handlerResponse;\n }\n\n // Default pass-through: update both the response cookie and the request\n // Cookie header so the downstream Server Component render is not stale.\n const requestHeaders: Headers = new Headers(request.headers);\n const updatedCookieHeader: string = replaceChunkedCookieInHeader(\n request.headers.get('cookie') ?? '',\n cookieName,\n refreshCookieUpdate.token,\n );\n requestHeaders.set('cookie', updatedCookieHeader);\n\n const response: NextResponse = NextResponse.next({request: {headers: requestHeaders}});\n setChunkedCookie(\n toChunkedCookieWriter(request, response.cookies),\n cookieName,\n refreshCookieUpdate.token,\n cookieOptions,\n );\n return response;\n };\n\nexport default thunderIDProxy;\n"],"mappings":"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"}
1
+ {"version":3,"file":"thunderIDProxy.cjs","names":["eqIdx: number","entries: {name: string; part: string}[]","CookieChunking","remaining: string[]","newParts: string[]","resolvedOptions: ThunderIDProxyOptions","resolvedConfig: ThunderIDNextConfig","decorateConfigWithNextEnv","url: URL","hasCallbackParams: boolean","tempSessionToken: string | undefined","SessionManager","verifiedSession: SessionTokenPayload | undefined","getSessionFromRequest","expiredSession: SessionTokenPayload | undefined","rawToken: string | undefined","getChunkedCookie","decoded: SessionTokenPayload","now: number","candidateSession: SessionTokenPayload | undefined","needsRefresh: boolean","REFRESH_BUFFER_SECONDS","activeSession: SessionTokenPayload | undefined","refreshCookieUpdate: {expiry: number; token: string} | undefined","handleRefreshToken","rawSessionCookie: string | undefined","sessionId: string | undefined","getSessionIdFromRequest","handlerResponse: NextResponse | void","referer: string | null","refererUrl: URL","requestUrl: URL","redirectUrl: string","NextResponse","cookieName: string","cookieName","requestHeaders: Headers","cleanResponse: NextResponse","requestHeaders","cookieOptions: ReturnType<typeof SessionManager.getSessionCookieOptions>","updatedCookieHeader: string","response: NextResponse"],"sources":["../../../../src/server/proxy/thunderIDProxy.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {CookieChunking, LogoutRecordStore} from '@thunderid/node';\nimport {NextRequest, NextResponse} from 'next/server';\nimport {REFRESH_BUFFER_SECONDS} from '../../constants/sessionConstants';\nimport {ThunderIDNextConfig} from '../../models/config';\nimport {\n ChunkedCookieOptions,\n ChunkedCookieWriter,\n deleteChunkedCookie,\n getChunkedCookie,\n setChunkedCookie,\n} from '../../utils/chunkedCookie';\nimport decorateConfigWithNextEnv from '../../utils/decorateConfigWithNextEnv';\nimport handleRefreshToken from '../../utils/handleRefreshToken';\nimport {setLogoutRecordStore} from '../../utils/loggedOutSessions';\nimport SessionManager, {SessionTokenPayload} from '../../utils/SessionManager';\nimport {getSessionFromRequest, getSessionIdFromRequest} from '../../utils/sessionUtils';\n\nexport type ThunderIDProxyOptions = Partial<ThunderIDNextConfig> & {\n /**\n * Where sessions ended by back-channel logout are recorded. Give the same store to\n * `backchannelLogout()`. Needed when more than one instance of the application runs, or when\n * this proxy runs on the Edge runtime, which shares no memory with the route handler. Defaults\n * to the memory of this process.\n */\n backchannelLogout?: {store?: LogoutRecordStore};\n};\n\nexport interface ThunderIDProxyContext {\n /** Get the session payload from JWT session if available */\n getSession: () => Promise<SessionTokenPayload | undefined>;\n /** Get the session ID from the current request */\n getSessionId: () => string | undefined;\n /** Check if the current request has a valid ThunderID session */\n isSignedIn: () => boolean;\n /**\n * Protect a route by redirecting unauthenticated users.\n * Redirect URL fallback order:\n * 1. options.redirect\n * 2. resolvedOptions.signInUrl\n * 3. resolvedOptions.defaultRedirect\n * 4. referer (if from same origin)\n * If none are available, falls back to '/'.\n */\n protectRoute: (routeOptions?: {redirect?: string}) => Promise<NextResponse | void>;\n}\n\ntype ThunderIDProxyHandler = (\n thunderid: ThunderIDProxyContext,\n req: NextRequest,\n) => Promise<NextResponse | void> | NextResponse | void;\n\n/**\n * Splits a raw Cookie header string into its `{name, part}` entries, where\n * `part` is the original `name=value` text (trimmed, never empty).\n */\nconst parseCookieHeaderParts = (cookieHeader: string): {name: string; part: string}[] =>\n cookieHeader\n .split(';')\n .map((p: string) => p.trim())\n .filter(Boolean)\n .map((part: string) => {\n const eqIdx: number = part.indexOf('=');\n const name: string = eqIdx === -1 ? part : part.slice(0, eqIdx).trim();\n return {name, part};\n });\n\n/**\n * Removes a cookie from a raw Cookie header string — the unchunked `name`\n * entry and/or any numbered `${name}.0`, `${name}.1`, ... chunk entries.\n */\nexport const removeChunkedCookieFromHeader = (cookieHeader: string, name: string): string => {\n const entries: {name: string; part: string}[] = parseCookieHeaderParts(cookieHeader);\n const toRemove = new Set<string>(\n CookieChunking.filterChunkNames(\n name,\n entries.map((e) => e.name),\n ),\n );\n\n return entries\n .filter((e) => !toRemove.has(e.name))\n .map((e) => e.part)\n .join('; ');\n};\n\n/**\n * Replaces a cookie's value inside a raw Cookie header string, splitting it\n * across numbered `${name}.0`, `${name}.1`, ... chunk entries once it would\n * exceed the ~4KB per-cookie limit browsers enforce. Any entry the cookie\n * doesn't appear in yet is appended; stale chunk entries a shrunk value no\n * longer needs are dropped.\n */\nexport const replaceChunkedCookieInHeader = (cookieHeader: string, name: string, value: string): string => {\n const remaining: string[] = parseCookieHeaderParts(removeChunkedCookieFromHeader(cookieHeader, name)).map(\n (e) => e.part,\n );\n const newParts: string[] = Object.entries(CookieChunking.split(name, value)).map(\n ([chunkName, chunkValue]: [string, string]) => `${chunkName}=${chunkValue}`,\n );\n\n return [...remaining, ...newParts].join('; ');\n};\n\n/**\n * Adapts a request/response pair into a {@link ChunkedCookieWriter}: existing\n * chunk names are discovered from the incoming request's cookies (what the\n * browser currently holds), while writes/deletes apply to the response.\n */\nconst toChunkedCookieWriter = (\n request: NextRequest,\n responseCookies: NextResponse['cookies'],\n): ChunkedCookieWriter => ({\n delete: (name: string): void => {\n responseCookies.delete(name);\n },\n getAll: (): {name: string}[] => request.cookies.getAll(),\n set: (name: string, value: string, options: ChunkedCookieOptions): void => {\n responseCookies.set(name, value, options);\n },\n});\n\n/**\n * ThunderID proxy that integrates authentication into your Next.js application.\n * Similar to Clerk's clerkMiddleware pattern.\n *\n * Proactively refreshes the access token when it is within REFRESH_BUFFER_SECONDS of\n * expiry so that Server Components always receive a fresh session. The refresh also\n * recovers expired tokens as long as a refresh token is present.\n *\n * The updated session cookie is written to:\n * - The response → browser stores the new cookie for subsequent requests.\n * - The forwarded request headers → the same-request Server Component render sees\n * the fresh token immediately without waiting for the next navigation.\n *\n * Token refresh requires baseUrl, clientId, and clientSecret. These are resolved from\n * the options argument first, then from the standard ThunderID environment variables\n * (NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_CLIENT_ID,\n * THUNDERID_CLIENT_SECRET). If none are available the refresh step is skipped silently.\n *\n * @param handler - Optional handler function to customize proxy behavior\n * @param options - Configuration options for the proxy\n * @returns Next.js middleware function\n *\n * @example\n * ```typescript\n * // middleware.ts - Basic usage (config read from env vars automatically)\n * import { thunderIDProxy } from '@thunderid/nextjs/server';\n *\n * export default thunderIDProxy();\n *\n * export const config = {\n * matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],\n * };\n * ```\n *\n * @example\n * ```typescript\n * // With route protection\n * import { thunderIDProxy, createRouteMatcher } from '@thunderid/nextjs/server';\n *\n * const isProtectedRoute = createRouteMatcher(['/dashboard(.*)']);\n *\n * export default thunderIDProxy(async (thunderid, req) => {\n * if (isProtectedRoute(req)) {\n * await thunderid.protectRoute();\n * }\n * });\n * ```\n */\nconst thunderIDProxy =\n (\n handler?: ThunderIDProxyHandler,\n options?: ThunderIDProxyOptions | ((req: NextRequest) => ThunderIDProxyOptions),\n ): ((request: NextRequest) => Promise<NextResponse>) =>\n async (request: NextRequest): Promise<NextResponse> => {\n const resolvedOptions: ThunderIDProxyOptions = typeof options === 'function' ? options(request) : options || {};\n\n setLogoutRecordStore(resolvedOptions.backchannelLogout?.store);\n\n // Resolve full config from passed options + environment variable fallbacks.\n const resolvedConfig: ThunderIDNextConfig = decorateConfigWithNextEnv(resolvedOptions as ThunderIDNextConfig);\n\n // ── OAuth callback detection ──────────────────────────────────────────────\n const url: URL = new URL(request.url);\n const hasCallbackParams: boolean = url.searchParams.has('code') && url.searchParams.has('state');\n\n let isValidOAuthCallback = false;\n if (hasCallbackParams && !url.searchParams.has('error')) {\n const tempSessionToken: string | undefined = request.cookies.get(\n SessionManager.getTempSessionCookieName(),\n )?.value;\n if (tempSessionToken) {\n try {\n await SessionManager.verifyTempSession(tempSessionToken);\n isValidOAuthCallback = true;\n } catch {\n isValidOAuthCallback = false;\n }\n }\n }\n\n // ── Session resolution ────────────────────────────────────────────────────\n // Step 1: Attempt to get a fully verified (signature + expiry) session.\n const verifiedSession: SessionTokenPayload | undefined = await getSessionFromRequest(request);\n\n // Step 2: If no verified session exists, verify the raw cookie's signature\n // without enforcing expiry. This allows the proxy to recover from an\n // expired access token as long as the JWT is authentic and a refresh token\n // is present. Skipping the signature check here would let a tampered cookie\n // drive identity-confusion attacks since handleRefreshToken reuses `sub`,\n // `sessionId`, and `organizationId` from the input payload when minting the\n // new session JWT.\n let expiredSession: SessionTokenPayload | undefined;\n if (!verifiedSession) {\n const rawToken: string | undefined = getChunkedCookie(request.cookies, SessionManager.getSessionCookieName());\n if (rawToken) {\n try {\n const decoded: SessionTokenPayload = await SessionManager.verifySessionTokenForRefresh(rawToken);\n if (decoded.refreshToken) {\n expiredSession = decoded;\n }\n } catch {\n // Forged, tampered, wrong type, or malformed — ignore.\n }\n }\n }\n\n // ── Token refresh ─────────────────────────────────────────────────────────\n const now: number = Math.floor(Date.now() / 1000);\n const candidateSession: SessionTokenPayload | undefined = verifiedSession ?? expiredSession;\n\n // Config is required to call the token endpoint.\n const hasRefreshConfig = !!(resolvedConfig.baseUrl && resolvedConfig.clientId && resolvedConfig.clientSecret);\n\n // Refresh when:\n // a) Token is verified but within the proactive buffer window, OR\n // b) Token has already expired but a refresh token is available.\n const needsRefresh: boolean =\n !isValidOAuthCallback &&\n hasRefreshConfig &&\n !!candidateSession?.refreshToken &&\n ((!!verifiedSession && verifiedSession.exp <= now + REFRESH_BUFFER_SECONDS) || !!expiredSession);\n\n let activeSession: SessionTokenPayload | undefined = verifiedSession;\n let refreshCookieUpdate: {expiry: number; token: string} | undefined;\n\n if (needsRefresh && candidateSession) {\n try {\n const {newSessionToken, sessionCookieExpiryTime} = await handleRefreshToken(candidateSession, {\n baseUrl: resolvedConfig.baseUrl!,\n clientId: resolvedConfig.clientId!,\n clientSecret: resolvedConfig.clientSecret!,\n sessionCookie: resolvedConfig.sessionCookie,\n });\n // Verify the newly minted token so activeSession reflects fresh claims.\n activeSession = await SessionManager.verifySessionToken(newSessionToken);\n refreshCookieUpdate = {expiry: sessionCookieExpiryTime, token: newSessionToken};\n } catch {\n // Refresh failed — clear the irrecoverable session.\n activeSession = undefined;\n }\n }\n\n // ── Session cleanup detection ─────────────────────────────────────────────\n // Mark stale cookies for deletion when the session is irrecoverable. Skipped\n // during OAuth callbacks where a session cookie may not exist yet.\n const rawSessionCookie: string | undefined = getChunkedCookie(\n request.cookies,\n SessionManager.getSessionCookieName(),\n );\n\n let shouldClearCookie = false;\n\n if (!isValidOAuthCallback && rawSessionCookie && !activeSession && !refreshCookieUpdate) {\n // A cookie was present but all resolution paths (verify, decode, refresh)\n // failed — the session is dead and cannot be recovered.\n shouldClearCookie = true;\n }\n\n const sessionId: string | undefined = activeSession?.sessionId ?? (await getSessionIdFromRequest(request));\n const isAuthenticated = !!activeSession;\n\n // ── Proxy context ─────────────────────────────────────────────────────────\n const thunderid: ThunderIDProxyContext = {\n getSession: async (): Promise<SessionTokenPayload | undefined> => activeSession,\n getSessionId: (): string | undefined => sessionId,\n isSignedIn: (): boolean => isAuthenticated,\n protectRoute: async (routeOptions?: {redirect?: string}): Promise<NextResponse | void> => {\n // Skip during a valid OAuth callback to avoid redirecting before the\n // callback action has had a chance to complete.\n if (isValidOAuthCallback) {\n return undefined;\n }\n\n if (!isAuthenticated) {\n const referer: string | null = request.headers.get('referer');\n let fallbackRedirect = '/';\n\n if (referer) {\n try {\n const refererUrl: URL = new URL(referer);\n const requestUrl: URL = new URL(request.url);\n if (refererUrl.origin === requestUrl.origin) {\n fallbackRedirect = refererUrl.pathname + refererUrl.search;\n }\n } catch {\n // Invalid referer — ignore.\n }\n }\n\n const redirectUrl: string = routeOptions?.redirect ?? resolvedConfig.signInUrl! ?? fallbackRedirect;\n\n return NextResponse.redirect(new URL(redirectUrl, request.url));\n }\n\n return undefined;\n },\n };\n\n // ── Handler ───────────────────────────────────────────────────────────────\n const handlerResponse: NextResponse | void = handler ? await handler(thunderid, request) : undefined;\n\n // ── Build final response ──────────────────────────────────────────────────\n if (shouldClearCookie) {\n const cookieName: string = SessionManager.getSessionCookieName();\n\n if (handlerResponse) {\n // Handler returned a response (e.g. a redirect from protectRoute).\n // Attach the deletion so the browser discards the stale cookie (and\n // every chunk of it).\n deleteChunkedCookie(toChunkedCookieWriter(request, handlerResponse.cookies), cookieName);\n return handlerResponse;\n }\n\n // Pass-through: strip the dead cookie from the forwarded request headers\n // so the same-request Server Component render sees no session at all.\n const requestHeaders: Headers = new Headers(request.headers);\n requestHeaders.set('cookie', removeChunkedCookieFromHeader(request.headers.get('cookie') ?? '', cookieName));\n const cleanResponse: NextResponse = NextResponse.next({request: {headers: requestHeaders}});\n deleteChunkedCookie(toChunkedCookieWriter(request, cleanResponse.cookies), cookieName);\n return cleanResponse;\n }\n\n if (!refreshCookieUpdate) {\n return handlerResponse ?? NextResponse.next();\n }\n\n // A token refresh occurred — the new session cookie must be applied to:\n // 1. The HTTP response so the browser stores the updated cookie.\n // 2. The forwarded request headers so the same-request Server Component\n // render reads the fresh session token instead of the expired one.\n const cookieName: string = SessionManager.getSessionCookieName();\n const cookieOptions: ReturnType<typeof SessionManager.getSessionCookieOptions> =\n SessionManager.getSessionCookieOptions(refreshCookieUpdate.expiry);\n\n if (handlerResponse) {\n // Handler returned a response (e.g. a redirect from protectRoute).\n // Attach the refresh cookie so the browser receives it even on redirects.\n setChunkedCookie(\n toChunkedCookieWriter(request, handlerResponse.cookies),\n cookieName,\n refreshCookieUpdate.token,\n cookieOptions,\n );\n return handlerResponse;\n }\n\n // Default pass-through: update both the response cookie and the request\n // Cookie header so the downstream Server Component render is not stale.\n const requestHeaders: Headers = new Headers(request.headers);\n const updatedCookieHeader: string = replaceChunkedCookieInHeader(\n request.headers.get('cookie') ?? '',\n cookieName,\n refreshCookieUpdate.token,\n );\n requestHeaders.set('cookie', updatedCookieHeader);\n\n const response: NextResponse = NextResponse.next({request: {headers: requestHeaders}});\n setChunkedCookie(\n toChunkedCookieWriter(request, response.cookies),\n cookieName,\n refreshCookieUpdate.token,\n cookieOptions,\n );\n return response;\n };\n\nexport default thunderIDProxy;\n"],"mappings":"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"}