Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/javascript/src/i18n/models/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface I18nTranslations {
/* Base Sign In */
'signin.heading': string;
'signin.subheading': string;
'signin.images.app_logo.alt': string;

/* Base Sign Up */
'signup.heading': string;
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/src/i18n/translations/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const translations: I18nTranslations = {
/* Base Sign In */
'signin.heading': 'Sign In',
'signin.subheading': 'Welcome back! Please sign in to continue.',
'signin.images.app_logo.alt': 'Application logo',

/* Base Sign Up */
'signup.heading': 'Sign Up',
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/src/i18n/translations/fr-FR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const translations: I18nTranslations = {
/* Base Sign In */
'signin.heading': 'Se connecter',
'signin.subheading': 'Entrez vos identifiants pour continuer.',
'signin.images.app_logo.alt': "Logo de l'application",

/* Base Sign Up */
'signup.heading': "S'inscrire",
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/src/i18n/translations/hi-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const translations: I18nTranslations = {
/* Base Sign In */
'signin.heading': 'साइन इन',
'signin.subheading': 'जारी रखने के लिए अपनी प्रमाणिक जानकारी दर्ज करें।',
'signin.images.app_logo.alt': 'एप्लिकेशन लोगो',

/* Base Sign Up */
'signup.heading': 'साइन अप',
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/src/i18n/translations/ja-JP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const translations: I18nTranslations = {
/* Base Sign In */
'signin.heading': 'ログイン',
'signin.subheading': '続行するには認証情報を入力してください。',
'signin.images.app_logo.alt': 'アプリケーションロゴ',

/* Base Sign Up */
'signup.heading': 'サインアップ',
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/src/i18n/translations/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const translations: I18nTranslations = {
/* Base Sign In */
'signin.heading': 'Entrar',
'signin.subheading': 'Digite suas credencias para continuar.',
'signin.images.app_logo.alt': 'Logotipo do aplicativo',

/* Base Sign Up */
'signup.heading': 'Cadastra-se',
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/src/i18n/translations/pt-PT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const translations: I18nTranslations = {
/* Base Sign In */
'signin.heading': 'Iniciar Sessão',
'signin.subheading': 'Introduza as suas credenciais para continuar.',
'signin.images.app_logo.alt': 'Logótipo da aplicação',

/* Base Sign Up */
'signup.heading': 'Registar-se',
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/src/i18n/translations/si-LK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const translations: I18nTranslations = {
/* Base Sign In */
'signin.heading': 'ලොග් වෙන්න',
'signin.subheading': 'ඉදිරියට යාමට ඔබේ සත්‍යාපන තොරතුරු ඇතුළත් කරන්න.',
'signin.images.app_logo.alt': 'යෙදුමේ ලාංඡනය',

/* Base Sign Up */
'signup.heading': 'ලියාපදිංචි වන්න',
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/src/i18n/translations/ta-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const translations: I18nTranslations = {
/* Base Sign In */
'signin.heading': 'உள்நுழை',
'signin.subheading': 'தொடர உங்கள் சான்றுகளை உள்ளிடவும்.',
'signin.images.app_logo.alt': 'பயன்பாட்டு லோகோ',

/* Base Sign Up */
'signup.heading': 'பதிவு செய்',
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/src/i18n/translations/te-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const translations: I18nTranslations = {
/* Base Sign In */
'signin.heading': 'సైన్ ఇన్ చేయండి',
'signin.subheading': 'కొనసాగించడానికి మీ వివరాలు ఇవ్వండి.',
'signin.images.app_logo.alt': 'అప్లికేషన్ లోగో',

/* Base Sign Up */
'signup.heading': 'సైన్ అప్ చేయండి',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ export type ThunderIDClientProviderProps = Partial<Omit<ThunderIDProviderProps,
Pick<ThunderIDProviderProps, 'baseUrl' | 'clientId'> & {
applicationId: ThunderIDContextProps['applicationId'];
clearSession: () => Promise<void>;
/**
* Server Action that fetches `GET /flow/meta` server-side, passed down to `FlowMetaProvider` so
* it never calls `baseUrl` directly from the browser — avoiding the CORS requirement that a
* direct client-side fetch to the ThunderID server would otherwise impose. Used for both the
* initial fetch (when `initialMeta` isn't seeded, e.g. it failed server-side) and subsequent
* `switchLanguage()` calls.
*/
fetchMeta?: (params: {applicationId?: string; language?: string}) => Promise<FlowMetadataResponse>;
handleOAuthCallback: (
code: string,
state: string,
Expand Down Expand Up @@ -84,6 +92,7 @@ const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps
scopes,
vendor,
initialMeta = null,
fetchMeta,
}: PropsWithChildren<ThunderIDClientProviderProps>) => {
const reRenderCheckRef: RefObject<boolean> = useRef(false);
const router: AppRouterInstance = useRouter();
Expand Down Expand Up @@ -382,7 +391,11 @@ const ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps
<ThunderIDContext.Provider value={contextValue}>
<ReactThunderIDContext.Provider value={reactContextValue}>
<I18nProvider preferences={preferences?.i18n}>
<FlowMetaProvider enabled={preferences?.resolveFromMeta !== false} initialMeta={initialMeta}>
<FlowMetaProvider
enabled={preferences?.resolveFromMeta !== false}
initialMeta={initialMeta}
fetchMeta={fetchMeta}
>
<ThemeProvider theme={preferences?.theme?.overrides} mode={getActiveTheme(preferences?.theme?.mode as any)}>
<FlowProvider>
<UserProvider profile={userProfile} onUpdateProfile={handleProfileUpdate} updateProfile={updateProfile}>
Expand Down
2 changes: 2 additions & 0 deletions packages/nextjs/src/server/ThunderIDProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import signInAction from './actions/signInAction';
import signOutAction from './actions/signOutAction';
import signUpAction from './actions/signUpAction';
import updateUserProfileAction from './actions/updateUserProfileAction';
import getFlowMetaAction from './actions/getFlowMetaAction';
import getClient from './getClient';
import ThunderIDClientProvider from '../client/contexts/ThunderID/ThunderIDProvider.js';
import {ThunderIDNextConfig} from '../models/config';
Expand Down Expand Up @@ -195,6 +196,7 @@ const ThunderIDServerProvider: FC<PropsWithChildren<ThunderIDServerProviderProps
applicationId={config?.applicationId}
baseUrl={config?.baseUrl}
initialMeta={flowMeta}
fetchMeta={getFlowMetaAction}
signIn={signInAction}
clearSession={clearSession}
refreshToken={refreshToken}
Expand Down
44 changes: 44 additions & 0 deletions packages/nextjs/src/server/actions/getFlowMetaAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2026 The ThunderID Authors
// SPDX-License-Identifier: Apache-2.0

'use server';

import {FlowMetadataResponse, FlowMetaType, getFlowMeta, resolveResourceEndpoint} from '@thunderid/node';
import {ThunderIDNextConfig} from '../../models/config';
import logger from '../../utils/logger';
import getClient from '../getClient';

/**
* Server Action that fetches `GET /flow/meta` server-side.
*
* `FlowMetaProvider` (from `@thunderid/react`) otherwise fetches this endpoint directly from the
* browser against `baseUrl`, which requires the ThunderID server to allow CORS for the app's
* origin. Routing it through this action instead means the browser never talks to `baseUrl`
* directly — the request happens server-to-server, same as `signInAction`/`signUpAction`.
*
* @param language - Optional language override, used by `FlowMetaProvider.switchLanguage()`.
*/
const getFlowMetaAction = async (params?: {
applicationId?: string;
language?: string;
}): Promise<FlowMetadataResponse> => {
const client = getClient();
const config: ThunderIDNextConfig = await client.getConfiguration();

const applicationId: string | undefined = params?.applicationId ?? config?.applicationId;

try {
return await getFlowMeta({
baseUrl: config?.baseUrl,
url: resolveResourceEndpoint('flowMeta', config),
...(applicationId ? {id: applicationId, type: FlowMetaType.App} : {}),
...(params?.language ? {language: params.language} : {}),
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error(`[getFlowMetaAction] Error fetching flow metadata: ${message}`);
throw error;
}
};

export default getFlowMetaAction;
45 changes: 27 additions & 18 deletions packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ export default defineNuxtModule<ThunderIDNuxtConfig>({
},
);

const privateConfig: {clientSecret: string; sessionSecret: string} = {
const privateConfig: {clientSecret: string; flowSecret: string; sessionSecret: string} = {
clientSecret: process.env.THUNDERID_CLIENT_SECRET || userOptions.clientSecret || '',
flowSecret: process.env.THUNDERID_FLOW_SECRET || userOptions.flowSecret || '',
sessionSecret: process.env.THUNDERID_SESSION_SECRET || userOptions.sessionSecret || '',
};

Expand All @@ -78,7 +79,7 @@ export default defineNuxtModule<ThunderIDNuxtConfig>({
options.runtimeConfig.thunderid = defu(
(options.runtimeConfig.thunderid as Record<string, unknown>) || {},
privateConfig,
) as {clientSecret: string; sessionSecret: string};
) as {clientSecret: string; flowSecret: string; sessionSecret: string};

options.runtimeConfig.public.thunderid = defu(
(options.runtimeConfig.public.thunderid as Record<string, unknown>) || {},
Expand Down Expand Up @@ -129,6 +130,13 @@ export default defineNuxtModule<ThunderIDNuxtConfig>({
`[${PACKAGE_NAME}] SECURITY: sessionSecret found in public config. Removed. Use THUNDERID_SESSION_SECRET env var.`,
);
}
if (publicThunderID?.flowSecret) {
delete publicThunderID.flowSecret;
// eslint-disable-next-line no-console
console.error(
`[${PACKAGE_NAME}] SECURITY: flowSecret found in public config. Removed. Use THUNDERID_FLOW_SECRET env var.`,
);
}

// Register server API routes
const serverRoutes: ServerRoute[] = [
Expand Down Expand Up @@ -158,6 +166,7 @@ export default defineNuxtModule<ThunderIDNuxtConfig>({
// ── Session / token ───────────────────────────────────────────────
{handler: resolve('./runtime/server/routes/auth/session/session.get'), route: '/api/auth/session'},
{handler: resolve('./runtime/server/routes/auth/session/token.get'), route: '/api/auth/token'},
{handler: resolve('./runtime/server/routes/auth/session/meta.get'), route: '/api/auth/meta'},
// ── User ──────────────────────────────────────────────────────────
{handler: resolve('./runtime/server/routes/auth/user/user.get'), route: '/api/auth/user'},
{handler: resolve('./runtime/server/routes/auth/user/profile.get'), route: '/api/auth/user/profile'},
Expand Down Expand Up @@ -208,41 +217,40 @@ export default defineNuxtModule<ThunderIDNuxtConfig>({
name: 'ThunderIDRoot',
});

// Register Nuxt-specific component containers with the `ThunderID` prefix.
// Register Nuxt-specific component containers under their bare names —
// matching the unprefixed names @thunderid/react and @thunderid/vue already
// export (`SignInButton`, `SignIn`, `UserProfile`, etc.).
//
// Each container lives at `./runtime/components/<Name>.ts` and:
// 1. Imports the corresponding BaseXxx from @thunderid/vue (not the Vue container).
// 2. Wires composables through `#imports` (Nuxt auto-import layer).
// 3. Uses `navigateTo` from `#app` for all navigation — SSR-safe, no window.location.
//
// This mirrors the Next.js SDK pattern where Base components come from
// @thunderid/react and host-specific containers live in the Next.js package.
//
// NOTE: Composables (useUser, useTheme, useFlow, useI18n) remain direct
// re-exports from @thunderid/vue via addImports above — only the components
// need Nuxt wrappers.

// ── Control flow ────────────────────────────────────────────────────────
addComponent({filePath: resolve('./runtime/components/control/SignedIn'), name: 'ThunderIDSignedIn'});
addComponent({filePath: resolve('./runtime/components/control/SignedOut'), name: 'ThunderIDSignedOut'});
addComponent({filePath: resolve('./runtime/components/control/Loading'), name: 'ThunderIDLoading'});
addComponent({filePath: resolve('./runtime/components/control/SignedIn'), name: 'SignedIn'});
addComponent({filePath: resolve('./runtime/components/control/SignedOut'), name: 'SignedOut'});
addComponent({filePath: resolve('./runtime/components/control/Loading'), name: 'Loading'});

// ── Action buttons ───────────────────────────────────────────────────────
addComponent({filePath: resolve('./runtime/components/actions/SignInButton'), name: 'ThunderIDSignInButton'});
addComponent({filePath: resolve('./runtime/components/actions/SignOutButton'), name: 'ThunderIDSignOutButton'});
addComponent({filePath: resolve('./runtime/components/actions/SignUpButton'), name: 'ThunderIDSignUpButton'});
addComponent({filePath: resolve('./runtime/components/actions/SignInButton'), name: 'SignInButton'});
addComponent({filePath: resolve('./runtime/components/actions/SignOutButton'), name: 'SignOutButton'});
addComponent({filePath: resolve('./runtime/components/actions/SignUpButton'), name: 'SignUpButton'});

// ── Embedded auth flows ──────────────────────────────────────────────────
addComponent({filePath: resolve('./runtime/components/auth/SignIn'), name: 'ThunderIDSignIn'});
addComponent({filePath: resolve('./runtime/components/auth/SignUp'), name: 'ThunderIDSignUp'});
addComponent({filePath: resolve('./runtime/components/auth/SignIn'), name: 'SignIn'});
addComponent({filePath: resolve('./runtime/components/auth/SignUp'), name: 'SignUp'});

// ── User ─────────────────────────────────────────────────────────────────
addComponent({filePath: resolve('./runtime/components/user/User'), name: 'ThunderIDUser'});
addComponent({filePath: resolve('./runtime/components/user/UserProfile'), name: 'ThunderIDUserProfile'});
addComponent({filePath: resolve('./runtime/components/user/UserDropdown'), name: 'ThunderIDUserDropdown'});
addComponent({filePath: resolve('./runtime/components/user/User'), name: 'User'});
addComponent({filePath: resolve('./runtime/components/user/UserProfile'), name: 'UserProfile'});
addComponent({filePath: resolve('./runtime/components/user/UserDropdown'), name: 'UserDropdown'});

// ── Auth callback ────────────────────────────────────────────────────────
addComponent({filePath: resolve('./runtime/components/auth/Callback'), name: 'ThunderIDCallback'});
addComponent({filePath: resolve('./runtime/components/auth/Callback'), name: 'Callback'});

// Tell Vite to pre-bundle the CJS-only packages that @thunderid/browser,
// @thunderid/javascript, and @thunderid/vue carry as external dependencies.
Expand Down Expand Up @@ -300,6 +308,7 @@ declare module '@nuxt/schema' {
interface RuntimeConfig {
thunderid: {
clientSecret: string;
flowSecret: string;
sessionSecret: string;
};
}
Expand Down
33 changes: 20 additions & 13 deletions packages/nuxt/src/runtime/components/ThunderIDRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
// SPDX-License-Identifier: Apache-2.0

import {generateFlattenedUserProfile} from '@thunderid/browser';
import type {AttributeSchema, UpdateMeProfileConfig, User, UserProfile} from '@thunderid/node';
import type {AttributeSchema, FlowMetadataResponse, UpdateMeProfileConfig, User, UserProfile} from '@thunderid/node';
import {FlowMetaProvider, FlowProvider, I18nProvider, ThemeProvider, UserProvider} from '@thunderid/vue';
import {defineComponent, h, type Component, type Ref, type SetupContext, type VNode} from 'vue';
import type {ThunderIDAuthState, ThunderIDNuxtConfig} from '../types';
import {getAuthStateKey, getUserProfileStateKey, getUserSchemaStateKey} from '../utils/stateKeys';
import {getAuthStateKey, getFlowMetaStateKey, getUserProfileStateKey, getUserSchemaStateKey} from '../utils/stateKeys';
import {useState, useRuntimeConfig} from '#imports';

/**
Expand Down Expand Up @@ -61,6 +61,9 @@ const ThunderIDRoot: Component = defineComponent({
> | null>(getUserSchemaStateKey(vendor));
// Used by onUpdateProfile to keep the top-level auth user claim in sync.
const authState: Ref<ThunderIDAuthState> = useState<ThunderIDAuthState>(getAuthStateKey(vendor));
const flowMetaState: Ref<FlowMetadataResponse | null> = useState<FlowMetadataResponse | null>(
getFlowMetaStateKey(vendor),
);

// ── Preferences from runtime config ────────────────────────────────────
const prefs: ThunderIDNuxtConfig['preferences'] | undefined = runtimeThunderIDConfig?.preferences;
Expand Down Expand Up @@ -101,8 +104,8 @@ const ThunderIDRoot: Component = defineComponent({
* Signature matches `UserProvider.updateProfile` exactly.
*
* On success, applies an optimistic local update via `onUpdateProfile`
* so consumers of `useUser()` (e.g. `<ThunderIDUserProfile>`) and
* `useThunderID().user` (e.g. `<ThunderIDUser>`) reflect the new value
* so consumers of `useUser()` (e.g. `<UserProfile>`) and
* `useThunderID().user` (e.g. `<User>`) reflect the new value
* without waiting for the next navigation/SSR refetch.
*/
const updateProfile = async (
Expand Down Expand Up @@ -138,15 +141,19 @@ const ThunderIDRoot: Component = defineComponent({
}
};

/**
* Fetches flow metadata via the `/api/auth/meta` Nitro route instead of `FlowMetaProvider`'s
* default direct browser-to-`baseUrl` fetch — so the browser never talks to the ThunderID
* server directly and no CORS configuration is required there. Used for both the initial
* fetch (when SSR seeding via `flowMetaState` didn't happen, e.g. it failed server-side) and
* subsequent `switchLanguage()` calls.
*/
const fetchMeta = async (params: {applicationId?: string; language?: string}): Promise<FlowMetadataResponse> =>
$fetch<FlowMetadataResponse>('/api/auth/meta', {
query: {...(params.language ? {language: params.language} : {})},
});

// ── Render tree — mirrors ThunderIDClientProvider (Next.js) ─────────────
//
// FlowMetaProvider is mounted unconditionally with `enabled: false` (V1
// platform default). It still provides `FLOW_META_KEY` to descendants so
// `useFlowMeta()` (called by `BaseSignUp`, v2 `BaseSignIn`,
// `BaseAcceptInvite`, `BaseInviteUser`) returns a real context with
// `meta: null` instead of throwing. When the Nuxt SDK gains a `platform`
// config option, derive `enabled` from it the same way `ThunderIDProvider`
// does (`enabled: platform === Platform.ThunderID`).
return (): VNode =>
h(
I18nProvider,
Expand All @@ -155,7 +162,7 @@ const ThunderIDRoot: Component = defineComponent({
default: (): VNode =>
h(
FlowMetaProvider,
{enabled: false},
{enabled: true, fetchMeta, initialMeta: flowMetaState.value},
{
default: (): VNode =>
h(
Expand Down
4 changes: 2 additions & 2 deletions packages/nuxt/src/runtime/components/actions/SignInButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import {useThunderID} from '#imports';
*
* @example
* ```vue
* <ThunderIDSignInButton />
* <ThunderIDSignInButton class="btn-primary">Log in</ThunderIDSignInButton>
* <SignInButton />
* <SignInButton class="btn-primary">Log in</SignInButton>
* ```
*/
const SignInButton: Component = defineComponent({
Expand Down
Loading
Loading