diff --git a/apps/login/next-env.d.ts b/apps/login/next-env.d.ts
index 1b3be0840..830fb594c 100755
--- a/apps/login/next-env.d.ts
+++ b/apps/login/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+///
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/login/next.config.mjs b/apps/login/next.config.mjs
index c0aab041c..5e85d3231 100755
--- a/apps/login/next.config.mjs
+++ b/apps/login/next.config.mjs
@@ -37,12 +37,9 @@ const nextConfig = {
output: process.env.NEXT_OUTPUT_MODE || undefined,
reactStrictMode: true,
experimental: {
- dynamicIO: true,
// Add React 19 compatibility optimizations
optimizePackageImports: ['@radix-ui/react-tooltip', '@heroicons/react'],
- },
- eslint: {
- ignoreDuringBuilds: true,
+ useCache: true,
},
// Improve SSR stability - not actually needed for React 19 SSR issues
// onDemandEntries: {
diff --git a/apps/login/package.json b/apps/login/package.json
index 0c7c7e8dc..3959aefbc 100644
--- a/apps/login/package.json
+++ b/apps/login/package.json
@@ -36,7 +36,7 @@
"deepmerge": "^4.3.1",
"lucide-react": "0.469.0",
"moment": "^2.29.4",
- "next": "15.4.0-canary.86",
+ "next": "15.5.7",
"next-intl": "^3.25.1",
"next-themes": "^0.2.1",
"nice-grpc": "2.0.1",
@@ -75,7 +75,7 @@
"dotenv-cli": "^8.0.0",
"env-cmd": "^10.1.0",
"eslint": "^8.57.0",
- "eslint-config-next": "15.4.0-canary.86",
+ "eslint-config-next": "15.5.7",
"eslint-config-prettier": "^9.1.0",
"gaxios": "^7.1.0",
"grpc-tools": "1.13.0",
diff --git a/apps/login/src/app/(login)/accounts/page.tsx b/apps/login/src/app/(login)/accounts/page.tsx
index 354d36f01..1b2516215 100644
--- a/apps/login/src/app/(login)/accounts/page.tsx
+++ b/apps/login/src/app/(login)/accounts/page.tsx
@@ -2,8 +2,8 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { SessionsList } from "@/components/sessions-list";
import { Translated } from "@/components/translated";
import { getAllSessionCookieIds } from "@/lib/cookies";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
-import { getBrandingSettings, getDefaultOrg, listSessions } from "@/lib/zitadel";
+import { getServiceConfig } from "@/lib/service-url";
+import { getBrandingSettings, getDefaultOrg, listSessions, ServiceConfig } from "@/lib/zitadel";
import { UserPlusIcon } from "@heroicons/react/24/outline";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { Metadata } from "next";
@@ -17,13 +17,11 @@ export async function generateMetadata(): Promise {
return { title: t("title") };
}
-async function loadSessions({ serviceUrl }: { serviceUrl: string }) {
+async function loadSessions({ serviceConfig }: { serviceConfig: ServiceConfig }) {
const cookieIds = await getAllSessionCookieIds();
if (cookieIds && cookieIds.length) {
- const response = await listSessions({
- serviceUrl,
- ids: cookieIds.filter((id) => !!id) as string[],
+ const response = await listSessions({ serviceConfig, ids: cookieIds.filter((id) => !!id) as string[],
});
return response?.sessions ?? [];
} else {
@@ -39,23 +37,19 @@ export default async function Page(props: { searchParams: Promise {
- return getUserByID({ serviceUrl, userId }).then((user) => {
+ return getUserByID({ serviceConfig, userId }).then((user) => {
const humanUser = user.user?.type.case === "human" ? user.user?.type.value : undefined;
return {
@@ -71,25 +69,21 @@ export default async function Page(props: { searchParams: Promise {
- return getAuthMethodsAndUser(serviceUrl, session);
+ return getAuthMethodsAndUser(serviceConfig.baseUrl, session);
});
}
async function loadSessionById(sessionId: string, organization?: string) {
const recent = await getSessionCookieById({ sessionId, organization });
- return getSession({
- serviceUrl,
- sessionId: recent.id,
+ return getSession({ serviceConfig, sessionId: recent.id,
sessionToken: recent.token,
}).then((sessionResponse) => {
- return getAuthMethodsAndUser(serviceUrl, sessionResponse.session);
+ return getAuthMethodsAndUser(serviceConfig.baseUrl, sessionResponse.session);
});
}
@@ -101,14 +95,10 @@ export default async function Page(props: { searchParams: Promise {
return resp.identityProviders;
diff --git a/apps/login/src/app/(login)/device/consent/page.tsx b/apps/login/src/app/(login)/device/consent/page.tsx
index dade4aa3e..a9da42c59 100644
--- a/apps/login/src/app/(login)/device/consent/page.tsx
+++ b/apps/login/src/app/(login)/device/consent/page.tsx
@@ -1,7 +1,7 @@
import { ConsentScreen } from "@/components/consent";
import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import { getBrandingSettings, getDefaultOrg, getDeviceAuthorizationRequest } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { headers } from "next/headers";
@@ -22,11 +22,9 @@ export default async function Page(props: { searchParams: Promise {
return resp.identityProviders;
});
- const branding = await getBrandingSettings({
- serviceUrl,
- organization,
+ const branding = await getBrandingSettings({ serviceConfig, organization,
});
return (
diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx
index 7c98f9d7e..3a4af52c8 100644
--- a/apps/login/src/app/(login)/loginname/page.tsx
+++ b/apps/login/src/app/(login)/loginname/page.tsx
@@ -2,7 +2,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { SignInWithIdp } from "@/components/sign-in-with-idp";
import { Translated } from "@/components/translated";
import { UsernameForm } from "@/components/username-form";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import { getActiveIdentityProviders, getBrandingSettings, getDefaultOrg, getLoginSettings } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { Metadata } from "next";
@@ -24,38 +24,28 @@ export default async function Page(props: { searchParams: Promise {
return resp.identityProviders;
});
- const branding = await getBrandingSettings({
- serviceUrl,
- organization: organization ?? defaultOrganization,
+ const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
});
return (
diff --git a/apps/login/src/app/(login)/logout/done/page.tsx b/apps/login/src/app/(login)/logout/done/page.tsx
index ecc95aff7..1405b8488 100644
--- a/apps/login/src/app/(login)/logout/done/page.tsx
+++ b/apps/login/src/app/(login)/logout/done/page.tsx
@@ -1,6 +1,6 @@
import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import { getBrandingSettings } from "@/lib/zitadel";
import { headers } from "next/headers";
@@ -8,13 +8,11 @@ export default async function Page(props: { searchParams: Promise }) {
const searchParams = await props.searchParams;
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const { organization } = searchParams;
- const branding = await getBrandingSettings({
- serviceUrl,
- organization,
+ const branding = await getBrandingSettings({ serviceConfig, organization,
});
return (
diff --git a/apps/login/src/app/(login)/logout/page.tsx b/apps/login/src/app/(login)/logout/page.tsx
index f35b5155b..d7a328d44 100644
--- a/apps/login/src/app/(login)/logout/page.tsx
+++ b/apps/login/src/app/(login)/logout/page.tsx
@@ -2,8 +2,9 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { SessionsClearList } from "@/components/sessions-clear-list";
import { Translated } from "@/components/translated";
import { getAllSessionCookieIds } from "@/lib/cookies";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
-import { getBrandingSettings, getDefaultOrg, listSessions } from "@/lib/zitadel";
+import { getServiceConfig } from "@/lib/service-url";
+import { getBrandingSettings, getDefaultOrg, listSessions, ServiceConfig } from "@/lib/zitadel";
+import { verifyJwt } from "@zitadel/client/node";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
@@ -11,17 +12,14 @@ import { headers } from "next/headers";
export async function generateMetadata(): Promise {
const t = await getTranslations("logout");
- return { title: t('title')};
+ return { title: t("title") };
}
-async function loadSessions({ serviceUrl }: { serviceUrl: string }) {
+async function loadSessions({ serviceConfig }: { serviceConfig: ServiceConfig }) {
const cookieIds = await getAllSessionCookieIds();
if (cookieIds && cookieIds.length) {
- const response = await listSessions({
- serviceUrl,
- ids: cookieIds.filter((id) => !!id) as string[],
- });
+ const response = await listSessions({ serviceConfig, ids: cookieIds.filter((id) => !!id) as string[] });
return response?.sessions ?? [];
} else {
console.info("No session cookie found.");
@@ -33,30 +31,46 @@ export default async function Page(props: { searchParams: Promise(
+ logoutToken,
+ `${serviceConfig.baseUrl}/oauth/v2/keys`,
+ {
+ instanceHost: serviceConfig.instanceHost,
+ publicHost: serviceConfig.publicHost,
+ },
+ );
+ console.log("logout token payload", payload);
+
+ if (payload.post_logout_redirect_uri && typeof payload.post_logout_redirect_uri === "string") {
+ postLogoutRedirectUri = payload.post_logout_redirect_uri;
+ }
+ if (payload.logout_hint && typeof payload.logout_hint === "string") {
+ logoutHint = payload.logout_hint;
+ }
+ } catch (error) {
+ console.error("Failed to verify logout token", error);
+ }
+ }
let defaultOrganization;
if (!organization) {
- const org: Organization | null = await getDefaultOrg({
- serviceUrl,
- });
+ const org: Organization | null = await getDefaultOrg({ serviceConfig });
if (org) {
defaultOrganization = org.id;
}
}
- let sessions = await loadSessions({ serviceUrl });
+ let sessions = await loadSessions({ serviceConfig });
- const branding = await getBrandingSettings({
- serviceUrl,
- organization: organization ?? defaultOrganization,
- });
+ const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization });
const params = new URLSearchParams();
diff --git a/apps/login/src/app/(login)/mfa/page.tsx b/apps/login/src/app/(login)/mfa/page.tsx
index 950fbdc72..9f3005735 100644
--- a/apps/login/src/app/(login)/mfa/page.tsx
+++ b/apps/login/src/app/(login)/mfa/page.tsx
@@ -5,7 +5,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getSession, listAuthenticationMethodTypes } from "@/lib/zitadel";
import { Metadata } from "next";
@@ -23,24 +23,20 @@ export default async function Page(props: { searchParams: Promise {
if (session && session.factors?.user?.id) {
- return listAuthenticationMethodTypes({
- serviceUrl,
- userId: session.factors.user.id,
+ return listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id,
}).then((methods) => {
return {
factors: session?.factors,
@@ -53,15 +49,11 @@ export default async function Page(props: { searchParams: Promise {
if (response?.session && response.session.factors?.user?.id) {
- return listAuthenticationMethodTypes({
- serviceUrl,
- userId: response.session.factors.user.id,
+ return listAuthenticationMethodTypes({ serviceConfig, userId: response.session.factors.user.id,
}).then((methods) => {
return {
factors: response.session?.factors,
@@ -72,9 +64,7 @@ export default async function Page(props: { searchParams: Promise {
- return getUserByID({ serviceUrl, userId }).then((user) => {
+ return getUserByID({ serviceConfig, userId }).then((user) => {
const humanUser = user.user?.type.case === "human" ? user.user?.type.value : undefined;
return {
@@ -79,9 +77,7 @@ export default async function Page(props: { searchParams: Promise {
return getAuthMethodsAndUser(sessionResponse.session);
});
}
- const branding = await getBrandingSettings({
- serviceUrl,
- organization,
+ const branding = await getBrandingSettings({ serviceConfig, organization,
});
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization: sessionWithData.factors?.user?.organizationId,
+ const loginSettings = await getLoginSettings({ serviceConfig, organization: sessionWithData.factors?.user?.organizationId,
});
const { valid } = isSessionValid(sessionWithData);
diff --git a/apps/login/src/app/(login)/otp/[method]/page.tsx b/apps/login/src/app/(login)/otp/[method]/page.tsx
index 1f0639171..56e7ba2c0 100644
--- a/apps/login/src/app/(login)/otp/[method]/page.tsx
+++ b/apps/login/src/app/(login)/otp/[method]/page.tsx
@@ -4,8 +4,8 @@ import { LoginOTP } from "@/components/login-otp";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies";
-import { getOriginalHost } from "@/lib/server/host";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getPublicHost } from "@/lib/server/host";
+import { getServiceConfig } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getLoginSettings, getSession } from "@/lib/zitadel";
import { Metadata } from "next";
@@ -25,8 +25,8 @@ export default async function Page(props: {
const searchParams = await props.searchParams;
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
- const host = await getOriginalHost();
+ const { serviceConfig } = getServiceConfig(_headers);
+ const host = getPublicHost(_headers);
const {
loginName, // send from password page
@@ -40,18 +40,11 @@ export default async function Page(props: {
const session = sessionId
? await loadSessionById(sessionId, organization)
- : await loadMostRecentSession({
- serviceUrl,
- sessionParams: { loginName, organization },
- });
+ : await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization } });
async function loadSessionById(sessionId: string, organization?: string) {
const recent = await getSessionCookieById({ sessionId, organization });
- return getSession({
- serviceUrl,
- sessionId: recent.id,
- sessionToken: recent.token,
- }).then((response) => {
+ return getSession({ serviceConfig, sessionId: recent.id, sessionToken: recent.token }).then((response) => {
if (response?.session) {
return response.session;
}
@@ -60,12 +53,12 @@ export default async function Page(props: {
// email links do not come with organization, thus we need to use the session's organization
const branding = await getBrandingSettings({
- serviceUrl,
+ serviceConfig,
organization: organization ?? session?.factors?.user?.organizationId,
});
const loginSettings = await getLoginSettings({
- serviceUrl,
+ serviceConfig,
organization: organization ?? session?.factors?.user?.organizationId,
});
diff --git a/apps/login/src/app/(login)/otp/[method]/set/page.tsx b/apps/login/src/app/(login)/otp/[method]/set/page.tsx
index 54dc2d376..6e9bedda0 100644
--- a/apps/login/src/app/(login)/otp/[method]/set/page.tsx
+++ b/apps/login/src/app/(login)/otp/[method]/set/page.tsx
@@ -5,7 +5,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { TotpRegister } from "@/components/totp-register";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session";
import { addOTPEmail, addOTPSMS, getBrandingSettings, getLoginSettings, registerTOTP } from "@/lib/zitadel";
import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
@@ -24,20 +24,14 @@ export default async function Page(props: {
const { method } = params;
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
- const branding = await getBrandingSettings({
- serviceUrl,
- organization,
+ const branding = await getBrandingSettings({ serviceConfig, organization,
});
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization,
+ const loginSettings = await getLoginSettings({ serviceConfig, organization,
});
- const session = await loadMostRecentSession({
- serviceUrl,
- sessionParams: {
+ const session = await loadMostRecentSession({ serviceConfig, sessionParams: {
loginName,
organization,
},
@@ -46,9 +40,7 @@ export default async function Page(props: {
let totpResponse: RegisterTOTPResponse | undefined, error: Error | undefined;
if (session && session.factors?.user?.id) {
if (method === "time-based") {
- await registerTOTP({
- serviceUrl,
- userId: session.factors.user.id,
+ await registerTOTP({ serviceConfig, userId: session.factors.user.id,
})
.then((resp) => {
if (resp) {
@@ -59,17 +51,13 @@ export default async function Page(props: {
error = err;
});
} else if (method === "sms") {
- await addOTPSMS({
- serviceUrl,
- userId: session.factors.user.id,
+ await addOTPSMS({ serviceConfig, userId: session.factors.user.id,
}).catch((_error) => {
// TODO: Throw this error?
new Error("Could not add OTP via SMS");
});
} else if (method === "email") {
- await addOTPEmail({
- serviceUrl,
- userId: session.factors.user.id,
+ await addOTPEmail({ serviceConfig, userId: session.factors.user.id,
}).catch((_error) => {
// TODO: Throw this error?
new Error("Could not add OTP via Email");
diff --git a/apps/login/src/app/(login)/passkey/page.tsx b/apps/login/src/app/(login)/passkey/page.tsx
index 0165cb70f..a0b08e709 100644
--- a/apps/login/src/app/(login)/passkey/page.tsx
+++ b/apps/login/src/app/(login)/passkey/page.tsx
@@ -4,7 +4,7 @@ import { LoginPasskey } from "@/components/login-passkey";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getSession } from "@/lib/zitadel";
import { Metadata } from "next";
@@ -22,20 +22,16 @@ export default async function Page(props: { searchParams: Promise {
if (response?.session) {
@@ -44,9 +40,7 @@ export default async function Page(props: { searchParams: Promise {
export default async function Page(props: { searchParams: Promise> }) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const searchParams = await props.searchParams;
const { loginName, organization, requestId } = searchParams;
// also allow no session to be found (ignoreUnkownUsername)
- const sessionFactors = await loadMostRecentSession({
- serviceUrl,
- sessionParams: {
+ const sessionFactors = await loadMostRecentSession({ serviceConfig, sessionParams: {
loginName,
organization,
},
});
- const branding = await getBrandingSettings({
- serviceUrl,
- organization,
+ const branding = await getBrandingSettings({ serviceConfig, organization,
});
- const passwordComplexity = await getPasswordComplexitySettings({
- serviceUrl,
- organization: sessionFactors?.factors?.user?.organizationId,
+ const passwordComplexity = await getPasswordComplexitySettings({ serviceConfig, organization: sessionFactors?.factors?.user?.organizationId,
});
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization: sessionFactors?.factors?.user?.organizationId,
+ const loginSettings = await getLoginSettings({ serviceConfig, organization: sessionFactors?.factors?.user?.organizationId,
});
return (
diff --git a/apps/login/src/app/(login)/password/page.tsx b/apps/login/src/app/(login)/password/page.tsx
index c016b32b3..3da584e21 100644
--- a/apps/login/src/app/(login)/password/page.tsx
+++ b/apps/login/src/app/(login)/password/page.tsx
@@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { PasswordForm } from "@/components/password-form";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getDefaultOrg, getLoginSettings } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
@@ -21,13 +21,11 @@ export default async function Page(props: { searchParams: Promise {
return resp.identityProviders.filter((idp) => {
return idp.options?.isAutoCreation || idp.options?.isCreationAllowed; // check if IDP allows to create account automatically or manual creation is allowed
diff --git a/apps/login/src/app/(login)/register/password/page.tsx b/apps/login/src/app/(login)/register/password/page.tsx
index 28f85c786..7c5cc3feb 100644
--- a/apps/login/src/app/(login)/register/password/page.tsx
+++ b/apps/login/src/app/(login)/register/password/page.tsx
@@ -1,7 +1,7 @@
import { DynamicTheme } from "@/components/dynamic-theme";
import { SetRegisterPasswordForm } from "@/components/set-register-password-form";
import { Translated } from "@/components/translated";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import {
getBrandingSettings,
getDefaultOrg,
@@ -18,12 +18,10 @@ export default async function Page(props: { searchParams: Promise {
return { title: t("title", { user: "" }) };
}
-async function loadSessionById(serviceUrl: string, sessionId: string, organization?: string) {
+async function loadSessionById(serviceConfig: ServiceConfig, sessionId: string, organization?: string) {
const recent = await getSessionCookieById({ sessionId, organization });
- return getSession({
- serviceUrl,
- sessionId: recent.id,
+ return getSession({ serviceConfig, sessionId: recent.id,
sessionToken: recent.token,
}).then((response) => {
if (response?.session) {
@@ -35,13 +33,11 @@ export default async function Page(props: { searchParams: Promise }) {
const searchParams = await props.searchParams;
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const { loginName, requestId, organization, sessionId } = searchParams;
- const branding = await getBrandingSettings({
- serviceUrl,
- organization,
+ const branding = await getBrandingSettings({ serviceConfig, organization,
});
// complete device authorization flow if device requestId is present
@@ -75,17 +71,13 @@ export default async function Page(props: { searchParams: Promise }) {
}
const sessionFactors = sessionId
- ? await loadSessionById(serviceUrl, sessionId, organization)
- : await loadMostRecentSession({
- serviceUrl,
- sessionParams: { loginName, organization },
+ ? await loadSessionById(serviceConfig, sessionId, organization)
+ : await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization },
});
let loginSettings;
if (!requestId) {
- loginSettings = await getLoginSettings({
- serviceUrl,
- organization,
+ loginSettings = await getLoginSettings({ serviceConfig, organization,
});
}
diff --git a/apps/login/src/app/(login)/u2f/page.tsx b/apps/login/src/app/(login)/u2f/page.tsx
index 5ca5542b0..897f74473 100644
--- a/apps/login/src/app/(login)/u2f/page.tsx
+++ b/apps/login/src/app/(login)/u2f/page.tsx
@@ -4,7 +4,7 @@ import { LoginPasskey } from "@/components/login-passkey";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getSession } from "@/lib/zitadel";
import { Metadata } from "next";
@@ -22,25 +22,19 @@ export default async function Page(props: { searchParams: Promise {
if (response?.session) {
diff --git a/apps/login/src/app/(login)/u2f/set/page.tsx b/apps/login/src/app/(login)/u2f/set/page.tsx
index 8d25864d3..1894f6dc0 100644
--- a/apps/login/src/app/(login)/u2f/set/page.tsx
+++ b/apps/login/src/app/(login)/u2f/set/page.tsx
@@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { RegisterU2f } from "@/components/register-u2f";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings } from "@/lib/zitadel";
import { Metadata } from "next";
@@ -21,19 +21,15 @@ export default async function Page(props: { searchParams: Promise }) {
const { userId, loginName, code, organization, requestId, invite, send } = searchParams;
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
- const branding = await getBrandingSettings({
- serviceUrl,
- organization,
- });
+ const branding = await getBrandingSettings({ serviceConfig, organization });
let sessionFactors;
let user: User | undefined;
@@ -43,7 +40,7 @@ export default async function Page(props: { searchParams: Promise }) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
async function sendEmail(userId: string) {
- const hostWithProtocol = await getOriginalHostWithProtocol();
+ const hostWithProtocol = await getPublicHostWithProtocol(_headers);
if (invite === "true") {
await sendInviteEmailCode({
@@ -70,7 +67,7 @@ export default async function Page(props: { searchParams: Promise }) {
if ("loginName" in searchParams) {
sessionFactors = await loadMostRecentSession({
- serviceUrl,
+ serviceConfig,
sessionParams: {
loginName,
organization,
@@ -85,10 +82,7 @@ export default async function Page(props: { searchParams: Promise }) {
await sendEmail(userId);
}
- const userResponse = await getUserByID({
- serviceUrl,
- userId,
- });
+ const userResponse = await getUserByID({ serviceConfig, userId });
if (userResponse) {
user = userResponse.user;
if (user?.type.case === "human") {
diff --git a/apps/login/src/app/(login)/verify/success/page.tsx b/apps/login/src/app/(login)/verify/success/page.tsx
index 225169d32..55e000654 100644
--- a/apps/login/src/app/(login)/verify/success/page.tsx
+++ b/apps/login/src/app/(login)/verify/success/page.tsx
@@ -1,7 +1,7 @@
import { DynamicTheme } from "@/components/dynamic-theme";
import { Translated } from "@/components/translated";
import { UserAvatar } from "@/components/user-avatar";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
@@ -11,18 +11,14 @@ export default async function Page(props: { searchParams: Promise }) {
const searchParams = await props.searchParams;
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const { loginName, organization, userId } = searchParams;
- const branding = await getBrandingSettings({
- serviceUrl,
- organization,
+ const branding = await getBrandingSettings({ serviceConfig, organization,
});
- const sessionFactors = await loadMostRecentSession({
- serviceUrl,
- sessionParams: { loginName, organization },
+ const sessionFactors = await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization },
}).catch((error) => {
console.warn("Error loading session:", error);
});
@@ -33,9 +29,7 @@ export default async function Page(props: { searchParams: Promise }) {
throw Error("Failed to get user id");
}
- const userResponse = await getUserByID({
- serviceUrl,
- userId: id,
+ const userResponse = await getUserByID({ serviceConfig, userId: id,
});
let user: User | undefined;
diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts
index ed61c43c3..3304c5e78 100644
--- a/apps/login/src/app/login/route.ts
+++ b/apps/login/src/app/login/route.ts
@@ -1,8 +1,8 @@
import { getAllSessions } from "@/lib/cookies";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import { validateAuthRequest, isRSCRequest } from "@/lib/auth-utils";
import { handleOIDCFlowInitiation, handleSAMLFlowInitiation, FlowInitiationParams } from "@/lib/server/flow-initiation";
-import { listSessions } from "@/lib/zitadel";
+import { listSessions, ServiceConfig } from "@/lib/zitadel";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
@@ -13,18 +13,15 @@ export const fetchCache = "default-no-store";
// Add this to prevent RSC requests
export const runtime = "nodejs";
-async function loadSessions({ serviceUrl, ids }: { serviceUrl: string; ids: string[] }): Promise {
- const response = await listSessions({
- serviceUrl,
- ids: ids.filter((id: string | undefined) => !!id),
- });
+async function loadSessions({ serviceConfig, ids }: { serviceConfig: ServiceConfig; ids: string[] }): Promise {
+ const response = await listSessions({ serviceConfig, ids: ids.filter((id: string | undefined) => !!id) });
return response?.sessions ?? [];
}
export async function GET(request: NextRequest) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const searchParams = request.nextUrl.searchParams;
@@ -43,17 +40,11 @@ export async function GET(request: NextRequest) {
const ids = sessionCookies.map((s) => s.id);
let sessions: Session[] = [];
if (ids && ids.length) {
- sessions = await loadSessions({ serviceUrl, ids });
+ sessions = await loadSessions({ serviceConfig, ids });
}
// Flow initiation - delegate to appropriate handler
- const flowParams: FlowInitiationParams = {
- serviceUrl,
- requestId,
- sessions,
- sessionCookies,
- request,
- };
+ const flowParams: FlowInitiationParams = { serviceConfig, requestId, sessions, sessionCookies, request };
if (requestId.startsWith("oidc_")) {
return handleOIDCFlowInitiation(flowParams);
diff --git a/apps/login/src/app/security/route.ts b/apps/login/src/app/security/route.ts
index 4a2b6d485..80ad072f6 100644
--- a/apps/login/src/app/security/route.ts
+++ b/apps/login/src/app/security/route.ts
@@ -1,5 +1,5 @@
import { createServiceForHost } from "@/lib/service";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import { Client } from "@zitadel/client";
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
import { headers } from "next/headers";
@@ -7,10 +7,10 @@ import { NextResponse } from "next/server";
export async function GET() {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const settingsService: Client =
- await createServiceForHost(SettingsService, serviceUrl);
+ await createServiceForHost(SettingsService, serviceConfig);
const settings = await settingsService
.getSecuritySettings({})
diff --git a/apps/login/src/i18n/request.ts b/apps/login/src/i18n/request.ts
index 5ddf7da97..b0e824b5c 100644
--- a/apps/login/src/i18n/request.ts
+++ b/apps/login/src/i18n/request.ts
@@ -1,5 +1,5 @@
import { LANGS, LANGUAGE_COOKIE_NAME, LANGUAGE_HEADER_NAME } from "@/lib/i18n";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import { getHostedLoginTranslation } from "@/lib/zitadel";
import { JsonObject } from "@zitadel/client";
import deepmerge from "deepmerge";
@@ -13,7 +13,7 @@ export default getRequestConfig(async () => {
let locale: string = fallback;
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const languageHeader = await (await headers()).get(LANGUAGE_HEADER_NAME);
if (languageHeader) {
@@ -34,9 +34,7 @@ export default getRequestConfig(async () => {
let translations: JsonObject | {} = {};
try {
- const i18nJSON = await getHostedLoginTranslation({
- serviceUrl,
- locale,
+ const i18nJSON = await getHostedLoginTranslation({ serviceConfig, locale,
organization: i18nOrganization,
});
diff --git a/apps/login/src/lib/deployment.test.ts b/apps/login/src/lib/deployment.test.ts
new file mode 100644
index 000000000..b8a7c288d
--- /dev/null
+++ b/apps/login/src/lib/deployment.test.ts
@@ -0,0 +1,77 @@
+import { describe, expect, test, beforeEach, afterEach } from "vitest";
+import { hasSystemUserCredentials, hasServiceUserToken } from "./deployment";
+
+describe("Deployment utilities", () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ // Reset environment before each test
+ process.env = { ...originalEnv };
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ });
+
+ describe("hasSystemUserCredentials", () => {
+ test("should return true when all system user credentials are present", () => {
+ process.env.AUDIENCE = "https://api.zitadel.cloud";
+ process.env.SYSTEM_USER_ID = "12345";
+ process.env.SYSTEM_USER_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n...";
+
+ expect(hasSystemUserCredentials()).toBe(true);
+ });
+
+ test("should return false when AUDIENCE is missing", () => {
+ process.env.AUDIENCE = undefined as any;
+ process.env.SYSTEM_USER_ID = "12345";
+ process.env.SYSTEM_USER_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n...";
+
+ expect(hasSystemUserCredentials()).toBe(false);
+ });
+
+ test("should return false when SYSTEM_USER_ID is missing", () => {
+ process.env.AUDIENCE = "https://api.zitadel.cloud";
+ process.env.SYSTEM_USER_ID = undefined as any;
+ process.env.SYSTEM_USER_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n...";
+
+ expect(hasSystemUserCredentials()).toBe(false);
+ });
+
+ test("should return false when SYSTEM_USER_PRIVATE_KEY is missing", () => {
+ process.env.AUDIENCE = "https://api.zitadel.cloud";
+ process.env.SYSTEM_USER_ID = "12345";
+ process.env.SYSTEM_USER_PRIVATE_KEY = undefined as any;
+
+ expect(hasSystemUserCredentials()).toBe(false);
+ });
+
+ test("should return false when all credentials are missing", () => {
+ process.env.AUDIENCE = undefined as any;
+ process.env.SYSTEM_USER_ID = undefined as any;
+ process.env.SYSTEM_USER_PRIVATE_KEY = undefined as any;
+
+ expect(hasSystemUserCredentials()).toBe(false);
+ });
+ });
+
+ describe("hasServiceUserToken", () => {
+ test("should return true when ZITADEL_SERVICE_USER_TOKEN is present", () => {
+ process.env.ZITADEL_SERVICE_USER_TOKEN = "token123";
+
+ expect(hasServiceUserToken()).toBe(true);
+ });
+
+ test("should return false when ZITADEL_SERVICE_USER_TOKEN is not set", () => {
+ process.env.ZITADEL_SERVICE_USER_TOKEN = undefined as any;
+
+ expect(hasServiceUserToken()).toBe(false);
+ });
+
+ test("should return false when ZITADEL_SERVICE_USER_TOKEN is empty string", () => {
+ process.env.ZITADEL_SERVICE_USER_TOKEN = "";
+
+ expect(hasServiceUserToken()).toBe(false);
+ });
+ });
+});
diff --git a/apps/login/src/lib/deployment.ts b/apps/login/src/lib/deployment.ts
new file mode 100644
index 000000000..943a57133
--- /dev/null
+++ b/apps/login/src/lib/deployment.ts
@@ -0,0 +1,24 @@
+/**
+ * Checks if system user credentials are available for JWT authentication.
+ *
+ * System user authentication requires:
+ * - AUDIENCE: The API audience for JWT authentication
+ * - SYSTEM_USER_ID: The system user's ID
+ * - SYSTEM_USER_PRIVATE_KEY: The private key for JWT signing
+ *
+ * Both multi-tenant and self-hosted deployments can use system user authentication.
+ *
+ * @returns true if system user credentials are present, false otherwise
+ */
+export function hasSystemUserCredentials(): boolean {
+ return !!process.env.AUDIENCE && !!process.env.SYSTEM_USER_ID && !!process.env.SYSTEM_USER_PRIVATE_KEY;
+}
+
+/**
+ * Checks if service user token is available for authentication.
+ *
+ * @returns true if ZITADEL_SERVICE_USER_TOKEN is present, false otherwise
+ */
+export function hasServiceUserToken(): boolean {
+ return !!process.env.ZITADEL_SERVICE_USER_TOKEN;
+}
diff --git a/apps/login/src/lib/oidc.ts b/apps/login/src/lib/oidc.ts
index 8bfaa3c0f..f1f0301da 100644
--- a/apps/login/src/lib/oidc.ts
+++ b/apps/login/src/lib/oidc.ts
@@ -1,25 +1,19 @@
import { Cookie } from "@/lib/cookies";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
-import { createCallback, getLoginSettings } from "@/lib/zitadel";
+import { createCallback, getLoginSettings, ServiceConfig } from "@/lib/zitadel";
import { create } from "@zitadel/client";
import { CreateCallbackRequestSchema, SessionSchema } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { isSessionValid } from "./session";
type LoginWithOIDCAndSession = {
- serviceUrl: string;
+ serviceConfig: ServiceConfig;
authRequest: string;
sessionId: string;
sessions: Session[];
sessionCookies: Cookie[];
};
-export async function loginWithOIDCAndSession({
- serviceUrl,
- authRequest,
- sessionId,
- sessions,
- sessionCookies,
-}: LoginWithOIDCAndSession): Promise<{ error: string } | { redirect: string }> {
+export async function loginWithOIDCAndSession({ serviceConfig, authRequest, sessionId, sessions, sessionCookies }: LoginWithOIDCAndSession): Promise<{ error: string } | { redirect: string }> {
console.log(`Login with session: ${sessionId} and authRequest: ${authRequest}`);
const selectedSession = sessions.find((s) => s.id === sessionId);
@@ -27,10 +21,7 @@ export async function loginWithOIDCAndSession({
if (selectedSession && selectedSession.id) {
console.log(`Found session ${selectedSession.id}`);
- const isValid = await isSessionValid({
- serviceUrl,
- session: selectedSession,
- });
+ const isValid = await isSessionValid({ serviceConfig, session: selectedSession });
console.log("Session is valid:", isValid);
@@ -61,9 +52,7 @@ export async function loginWithOIDCAndSession({
};
try {
- const { callbackUrl } = await createCallback({
- serviceUrl,
- req: create(CreateCallbackRequestSchema, {
+ const { callbackUrl } = await createCallback({ serviceConfig, req: create(CreateCallbackRequestSchema, {
authRequestId: authRequest,
callbackKind: {
case: "session",
@@ -81,10 +70,7 @@ export async function loginWithOIDCAndSession({
// handle already handled gracefully as these could come up if old emails with requestId are used (reset password, register emails etc.)
console.error(error);
if (error && typeof error === "object" && "code" in error && error?.code === 9) {
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization: selectedSession.factors?.user?.organizationId,
- });
+ const loginSettings = await getLoginSettings({ serviceConfig, organization: selectedSession.factors?.user?.organizationId });
if (loginSettings?.defaultRedirectUri) {
return { redirect: loginSettings.defaultRedirectUri };
diff --git a/apps/login/src/lib/saml.ts b/apps/login/src/lib/saml.ts
index d43c26496..4de72d2de 100644
--- a/apps/login/src/lib/saml.ts
+++ b/apps/login/src/lib/saml.ts
@@ -1,6 +1,6 @@
import { Cookie } from "@/lib/cookies";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
-import { createResponse, getLoginSettings } from "@/lib/zitadel";
+import { createResponse, getLoginSettings, ServiceConfig } from "@/lib/zitadel";
import { create } from "@zitadel/client";
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
@@ -9,7 +9,7 @@ import { v4 as uuidv4 } from "uuid";
import { isSessionValid } from "./session";
type LoginWithSAMLAndSession = {
- serviceUrl: string;
+ serviceConfig: ServiceConfig;
samlRequest: string;
sessionId: string;
sessions: Session[];
@@ -82,13 +82,7 @@ export async function getSAMLFormCookie(uid: string): Promise {
}
}
-export async function loginWithSAMLAndSession({
- serviceUrl,
- samlRequest,
- sessionId,
- sessions,
- sessionCookies,
-}: LoginWithSAMLAndSession): Promise<{ error: string } | { redirect: string }> {
+export async function loginWithSAMLAndSession({ serviceConfig, samlRequest, sessionId, sessions, sessionCookies }: LoginWithSAMLAndSession): Promise<{ error: string } | { redirect: string }> {
console.log(`Login with session: ${sessionId} and samlRequest: ${samlRequest}`);
const selectedSession = sessions.find((s) => s.id === sessionId);
@@ -96,10 +90,7 @@ export async function loginWithSAMLAndSession({
if (selectedSession && selectedSession.id) {
console.log(`Found session ${selectedSession.id}`);
- const isValid = await isSessionValid({
- serviceUrl,
- session: selectedSession,
- });
+ const isValid = await isSessionValid({ serviceConfig, session: selectedSession });
console.log("Session is valid:", isValid);
@@ -129,9 +120,7 @@ export async function loginWithSAMLAndSession({
// works not with _rsc request
try {
- const { url } = await createResponse({
- serviceUrl,
- req: create(CreateResponseRequestSchema, {
+ const { url } = await createResponse({ serviceConfig, req: create(CreateResponseRequestSchema, {
samlRequestId: samlRequest,
responseKind: {
case: "session",
@@ -149,10 +138,7 @@ export async function loginWithSAMLAndSession({
console.error(error);
if (error && typeof error === "object" && "code" in error && error?.code === 9) {
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization: selectedSession.factors?.user?.organizationId,
- });
+ const loginSettings = await getLoginSettings({ serviceConfig, organization: selectedSession.factors?.user?.organizationId });
if (loginSettings?.defaultRedirectUri) {
return { redirect: loginSettings.defaultRedirectUri };
diff --git a/apps/login/src/lib/self.ts b/apps/login/src/lib/self.ts
index df8508c29..2aeacaf33 100644
--- a/apps/login/src/lib/self.ts
+++ b/apps/login/src/lib/self.ts
@@ -3,14 +3,11 @@
import { createUserServiceClient } from "@zitadel/client/v2";
import { headers } from "next/headers";
import { getSessionCookieById } from "./cookies";
-import { getServiceUrlFromHeaders } from "./service-url";
-import { createServerTransport, getSession } from "./zitadel";
+import { getServiceConfig } from "./service-url";
+import { createServerTransport, getSession, ServiceConfig } from "./zitadel";
-const myUserService = async (serviceUrl: string, sessionToken: string) => {
- const transportPromise = await createServerTransport(
- sessionToken,
- serviceUrl,
- );
+const myUserService = async (serviceConfig: ServiceConfig, sessionToken: string) => {
+ const transportPromise = await createServerTransport(sessionToken, serviceConfig);
return createUserServiceClient(transportPromise);
};
@@ -22,13 +19,11 @@ export async function setMyPassword({
password: string;
}) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const sessionCookie = await getSessionCookieById({ sessionId });
- const { session } = await getSession({
- serviceUrl,
- sessionId: sessionCookie.id,
+ const { session } = await getSession({ serviceConfig, sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
@@ -36,7 +31,7 @@ export async function setMyPassword({
return { error: "Could not load session" };
}
- const service = await myUserService(serviceUrl, `${sessionCookie.token}`);
+ const service = await myUserService(serviceConfig, `${sessionCookie.token}`);
if (!session?.factors?.user?.id) {
return { error: "No user id found in session" };
diff --git a/apps/login/src/lib/server/auth-flow.ts b/apps/login/src/lib/server/auth-flow.ts
index 5e0bdd0bf..369db766b 100644
--- a/apps/login/src/lib/server/auth-flow.ts
+++ b/apps/login/src/lib/server/auth-flow.ts
@@ -3,8 +3,8 @@
import { getAllSessions } from "@/lib/cookies";
import { loginWithOIDCAndSession } from "@/lib/oidc";
import { loginWithSAMLAndSession } from "@/lib/saml";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
-import { listSessions } from "@/lib/zitadel";
+import { getServiceConfig } from "@/lib/service-url";
+import { listSessions, ServiceConfig } from "@/lib/zitadel";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { headers } from "next/headers";
@@ -14,10 +14,8 @@ export interface AuthFlowParams {
organization?: string;
}
-async function loadSessions({ serviceUrl, ids }: { serviceUrl: string; ids: string[] }): Promise {
- const response = await listSessions({
- serviceUrl,
- ids: ids.filter((id: string | undefined) => !!id),
+async function loadSessions({ serviceConfig, ids }: { serviceConfig: ServiceConfig; ids: string[] }): Promise {
+ const response = await listSessions({ serviceConfig, ids: ids.filter((id: string | undefined) => !!id),
});
return response?.sessions ?? [];
@@ -33,21 +31,19 @@ export async function completeAuthFlow(command: AuthFlowParams): Promise<{ error
const { sessionId, requestId } = command;
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const sessionCookies = await getAllSessions();
const ids = sessionCookies.map((s) => s.id);
let sessions: Session[] = [];
if (ids && ids.length) {
- sessions = await loadSessions({ serviceUrl, ids });
+ sessions = await loadSessions({ serviceConfig, ids });
}
if (requestId.startsWith("oidc_")) {
// Complete OIDC flow
- const result = await loginWithOIDCAndSession({
- serviceUrl,
- authRequest: requestId.replace("oidc_", ""),
+ const result = await loginWithOIDCAndSession({ serviceConfig, authRequest: requestId.replace("oidc_", ""),
sessionId,
sessions,
sessionCookies,
@@ -62,9 +58,7 @@ export async function completeAuthFlow(command: AuthFlowParams): Promise<{ error
return result;
} else if (requestId.startsWith("saml_")) {
// Complete SAML flow
- const result = await loginWithSAMLAndSession({
- serviceUrl,
- samlRequest: requestId.replace("saml_", ""),
+ const result = await loginWithSAMLAndSession({ serviceConfig, samlRequest: requestId.replace("saml_", ""),
sessionId,
sessions,
sessionCookies,
diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts
index 37c3edd3c..abd1025c3 100644
--- a/apps/login/src/lib/server/cookie.ts
+++ b/apps/login/src/lib/server/cookie.ts
@@ -14,7 +14,7 @@ import { Challenges, RequestChallenges } from "@zitadel/proto/zitadel/session/v2
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { headers } from "next/headers";
-import { getServiceUrlFromHeaders } from "../service-url";
+import { getServiceConfig } from "../service-url";
type CustomCookieData = {
id: string;
@@ -46,7 +46,7 @@ export async function createSessionAndUpdateCookie(command: {
lifetime?: Duration;
}): Promise {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
let sessionLifetime = command.lifetime;
@@ -59,16 +59,12 @@ export async function createSessionAndUpdateCookie(command: {
} as Duration; // for usecases where the lifetime is not specified (user discovery)
}
- const createdSession = await createSessionFromChecks({
- serviceUrl,
- checks: command.checks,
+ const createdSession = await createSessionFromChecks({ serviceConfig, checks: command.checks,
lifetime: sessionLifetime,
});
if (createdSession) {
- return getSession({
- serviceUrl,
- sessionId: createdSession.sessionId,
+ return getSession({ serviceConfig, sessionId: createdSession.sessionId,
sessionToken: createdSession.sessionToken,
}).then(async (response) => {
if (response?.session && response.session?.factors?.user?.loginName) {
@@ -89,7 +85,7 @@ export async function createSessionAndUpdateCookie(command: {
sessionCookie.organization = response.session.factors.user.organizationId;
}
- const securitySettings = await getSecuritySettings({ serviceUrl });
+ const securitySettings = await getSecuritySettings({ serviceConfig });
const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
await addSessionToCookie({ session: sessionCookie, iFrameEnabled });
@@ -119,7 +115,7 @@ export async function createSessionForIdpAndUpdateCookie({
lifetime?: Duration;
}): Promise {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
let sessionLifetime = lifetime;
@@ -132,9 +128,7 @@ export async function createSessionForIdpAndUpdateCookie({
} as Duration;
}
- const createdSession = await createSessionForUserIdAndIdpIntent({
- serviceUrl,
- userId,
+ const createdSession = await createSessionForUserIdAndIdpIntent({ serviceConfig, userId,
idpIntent,
lifetime: sessionLifetime,
}).catch((error: ErrorDetail | CredentialsCheckError) => {
@@ -152,9 +146,7 @@ export async function createSessionForIdpAndUpdateCookie({
throw "Could not create session";
}
- const { session } = await getSession({
- serviceUrl,
- sessionId: createdSession.sessionId,
+ const { session } = await getSession({ serviceConfig, sessionId: createdSession.sessionId,
sessionToken: createdSession.sessionToken,
});
@@ -180,7 +172,7 @@ export async function createSessionForIdpAndUpdateCookie({
sessionCookie.organization = session.factors.user.organizationId;
}
- const securitySettings = await getSecuritySettings({ serviceUrl });
+ const securitySettings = await getSecuritySettings({ serviceConfig });
const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
return addSessionToCookie({ session: sessionCookie, iFrameEnabled }).then(() => {
@@ -200,11 +192,9 @@ export async function setSessionAndUpdateCookie(command: {
lifetime: Duration;
}) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
- return setSession({
- serviceUrl,
- sessionId: command.recentCookie.id,
+ return setSession({ serviceConfig, sessionId: command.recentCookie.id,
sessionToken: command.recentCookie.token,
challenges: command.challenges,
checks: command.checks,
@@ -227,9 +217,7 @@ export async function setSessionAndUpdateCookie(command: {
sessionCookie.requestId = command.requestId;
}
- return getSession({
- serviceUrl,
- sessionId: sessionCookie.id,
+ return getSession({ serviceConfig, sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
}).then(async (response) => {
if (!response?.session || !response.session.factors?.user?.loginName) {
@@ -252,7 +240,7 @@ export async function setSessionAndUpdateCookie(command: {
newCookie.requestId = sessionCookie.requestId;
}
- const securitySettings = await getSecuritySettings({ serviceUrl });
+ const securitySettings = await getSecuritySettings({ serviceConfig });
const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
return updateSessionCookie({
diff --git a/apps/login/src/lib/server/device.ts b/apps/login/src/lib/server/device.ts
index 5e36facfc..70442d10a 100644
--- a/apps/login/src/lib/server/device.ts
+++ b/apps/login/src/lib/server/device.ts
@@ -2,19 +2,17 @@
import { authorizeOrDenyDeviceAuthorization } from "@/lib/zitadel";
import { headers } from "next/headers";
-import { getServiceUrlFromHeaders } from "../service-url";
+import { getServiceConfig } from "../service-url";
export async function completeDeviceAuthorization(
deviceAuthorizationId: string,
session?: { sessionId: string; sessionToken: string },
) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
// without the session, device auth request is denied
- return authorizeOrDenyDeviceAuthorization({
- serviceUrl,
- deviceAuthorizationId,
+ return authorizeOrDenyDeviceAuthorization({ serviceConfig, deviceAuthorizationId,
session,
});
}
diff --git a/apps/login/src/lib/server/flow-initiation.ts b/apps/login/src/lib/server/flow-initiation.ts
index ab747d1cf..48349557f 100644
--- a/apps/login/src/lib/server/flow-initiation.ts
+++ b/apps/login/src/lib/server/flow-initiation.ts
@@ -9,6 +9,7 @@ import {
getSAMLRequest,
getSecuritySettings,
startIdentityProviderFlow,
+ ServiceConfig,
} from "@/lib/zitadel";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
import { idpTypeToSlug } from "@/lib/idp";
@@ -47,7 +48,7 @@ const gotoAccounts = ({
};
export interface FlowInitiationParams {
- serviceUrl: string;
+ serviceConfig: ServiceConfig;
requestId: string;
sessions: Session[];
sessionCookies: any[];
@@ -58,11 +59,9 @@ export interface FlowInitiationParams {
* Handle OIDC flow initiation
*/
export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Promise {
- const { serviceUrl, requestId, sessions, sessionCookies, request } = params;
+ const { serviceConfig, requestId, sessions, sessionCookies, request } = params;
- const { authRequest } = await getAuthRequest({
- serviceUrl,
- authRequestId: requestId.replace("oidc_", ""),
+ const { authRequest } = await getAuthRequest({ serviceConfig, authRequestId: requestId.replace("oidc_", ""),
});
let organization = "";
@@ -85,9 +84,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
console.log("Extracted org domain:", orgDomain);
if (orgDomain) {
- const orgs = await getOrgsByDomain({
- serviceUrl,
- domain: orgDomain,
+ const orgs = await getOrgsByDomain({ serviceConfig, domain: orgDomain,
});
if (orgs.result && orgs.result.length === 1) {
@@ -102,9 +99,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
const matched = IDP_SCOPE_REGEX.exec(idpScope);
idpId = matched?.[1] ?? "";
- const identityProviders = await getActiveIdentityProviders({
- serviceUrl,
- orgId: organization ? organization : undefined,
+ const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization ? organization : undefined,
}).then((resp) => {
return resp.identityProviders;
});
@@ -137,9 +132,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
params.set("organization", organization);
}
- let url: string | null = await startIdentityProviderFlow({
- serviceUrl,
- idpId,
+ let url: string | null = await startIdentityProviderFlow({ serviceConfig, idpId,
urls: {
successUrl: `${origin}/idp/${provider}/process?` + new URLSearchParams(params),
failureUrl: `${origin}/idp/${provider}/failure?` + new URLSearchParams(params),
@@ -215,13 +208,9 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
}
return NextResponse.redirect(loginNameUrl);
} else if (authRequest.prompt.includes(Prompt.NONE)) {
- const securitySettings = await getSecuritySettings({
- serviceUrl,
- });
+ const securitySettings = await getSecuritySettings({ serviceConfig, });
- const selectedSession = await findValidSession({
- serviceUrl,
- sessions,
+ const selectedSession = await findValidSession({ serviceConfig, sessions,
authRequest,
});
@@ -251,9 +240,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
sessionToken: cookie.token,
};
- const { callbackUrl } = await createCallback({
- serviceUrl,
- req: create(CreateCallbackRequestSchema, {
+ const { callbackUrl } = await createCallback({ serviceConfig, req: create(CreateCallbackRequestSchema, {
authRequestId: requestId.replace("oidc_", ""),
callbackKind: {
case: "session",
@@ -275,9 +262,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
return callbackResponse;
} else {
- let selectedSession = await findValidSession({
- serviceUrl,
- sessions,
+ let selectedSession = await findValidSession({ serviceConfig, sessions,
authRequest,
});
@@ -305,9 +290,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
};
try {
- const { callbackUrl } = await createCallback({
- serviceUrl,
- req: create(CreateCallbackRequestSchema, {
+ const { callbackUrl } = await createCallback({ serviceConfig, req: create(CreateCallbackRequestSchema, {
authRequestId: requestId.replace("oidc_", ""),
callbackKind: {
case: "session",
@@ -359,11 +342,9 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
* Handle SAML flow initiation
*/
export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Promise {
- const { serviceUrl, requestId, sessions, sessionCookies, request } = params;
+ const { serviceConfig, requestId, sessions, sessionCookies, request } = params;
- const { samlRequest } = await getSAMLRequest({
- serviceUrl,
- samlRequestId: requestId.replace("saml_", ""),
+ const { samlRequest } = await getSAMLRequest({ serviceConfig, samlRequestId: requestId.replace("saml_", ""),
});
if (!samlRequest) {
@@ -378,9 +359,7 @@ export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Pr
}
// Try to find a valid session
- let selectedSession = await findValidSession({
- serviceUrl,
- sessions,
+ let selectedSession = await findValidSession({ serviceConfig, sessions,
samlRequest,
});
@@ -410,9 +389,7 @@ export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Pr
};
try {
- const { url, binding } = await createResponse({
- serviceUrl,
- req: create(CreateResponseRequestSchema, {
+ const { url, binding } = await createResponse({ serviceConfig, req: create(CreateResponseRequestSchema, {
samlRequestId: requestId.replace("saml_", ""),
responseKind: {
case: "session",
diff --git a/apps/login/src/lib/server/host.test.ts b/apps/login/src/lib/server/host.test.ts
index dfd0a04a5..941ac1755 100644
--- a/apps/login/src/lib/server/host.test.ts
+++ b/apps/login/src/lib/server/host.test.ts
@@ -1,10 +1,5 @@
import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
-import { getOriginalHost, getOriginalHostWithProtocol } from "./host";
-
-// Mock the Next.js headers function
-vi.mock("next/headers", () => ({
- headers: vi.fn(),
-}));
+import { getInstanceHost, getPublicHostWithProtocol, getPublicHost } from "./host";
describe("Host utility functions", () => {
beforeEach(() => {
@@ -15,151 +10,94 @@ describe("Host utility functions", () => {
vi.restoreAllMocks();
});
- describe("getOriginalHost", () => {
- test("should return x-forwarded-host when available", async () => {
- const { headers } = await import("next/headers");
+ describe("getInstanceHost", () => {
+ test("should use x-zitadel-instance-host when available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
- if (key === "x-forwarded-host") return "zitadel.com";
- if (key === "x-original-host") return "backup.com";
- if (key === "host") return "internal.vercel.app";
+ if (key === "x-zitadel-instance-host") return "instance.zitadel.cloud";
+ if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
return null;
}),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHost();
- expect(result).toBe("zitadel.com");
- expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
+ const result = getInstanceHost(mockHeaders);
+ expect(result).toBe("instance.zitadel.cloud");
+ expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
});
- test("should fall back to x-original-host when x-forwarded-host is not available", async () => {
- const { headers } = await import("next/headers");
+ test("should use x-zitadel-forward-host when x-zitadel-instance-host is not available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
- if (key === "x-forwarded-host") return null;
- if (key === "x-original-host") return "original.com";
- if (key === "host") return "internal.vercel.app";
+ if (key === "x-zitadel-instance-host") return null;
+ if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
return null;
}),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHost();
- expect(result).toBe("original.com");
- expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
- expect(mockHeaders.get).toHaveBeenCalledWith("x-original-host");
+ const result = getInstanceHost(mockHeaders);
+ expect(result).toBe("forward.zitadel.cloud");
+ expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
+ expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
});
- test("should fall back to host when forwarded headers are not available", async () => {
- const { headers } = await import("next/headers");
+ test("should return null when neither x-zitadel-instance-host nor x-zitadel-forward-host are available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
- if (key === "x-forwarded-host") return null;
- if (key === "x-original-host") return null;
- if (key === "host") return "fallback.com";
+ if (key === "x-zitadel-instance-host") return null;
+ if (key === "x-zitadel-forward-host") return null;
+ if (key === "x-forwarded-host") return "accounts.mycompany.com";
+ if (key === "host") return "internal.server";
return null;
}),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHost();
- expect(result).toBe("fallback.com");
- expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
- expect(mockHeaders.get).toHaveBeenCalledWith("x-original-host");
- expect(mockHeaders.get).toHaveBeenCalledWith("host");
- });
-
- test("should throw error when no host is found", async () => {
- const { headers } = await import("next/headers");
- const mockHeaders = {
- get: vi.fn(() => null),
- };
-
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- await expect(getOriginalHost()).rejects.toThrow("No host found in headers");
- });
-
- test("should throw error when host is empty string", async () => {
- const { headers } = await import("next/headers");
- const mockHeaders = {
- get: vi.fn(() => ""),
- };
-
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- await expect(getOriginalHost()).rejects.toThrow("No host found in headers");
- });
-
- test("should throw error when host is not a string", async () => {
- const { headers } = await import("next/headers");
- const mockHeaders = {
- get: vi.fn(() => 123),
- };
-
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- await expect(getOriginalHost()).rejects.toThrow("No host found in headers");
+ const result = getInstanceHost(mockHeaders);
+ expect(result).toBeNull();
+ expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
+ expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
});
});
- describe("getOriginalHostWithProtocol", () => {
- test("should return https for production domain", async () => {
- const { headers } = await import("next/headers");
+ describe("getPublicHostWithProtocol", () => {
+ test("should return https for production domain", () => {
const mockHeaders = {
get: vi.fn(() => "zitadel.com"),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHostWithProtocol();
+ const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://zitadel.com");
});
- test("should return http for localhost", async () => {
- const { headers } = await import("next/headers");
+ test("should return http for localhost", () => {
const mockHeaders = {
get: vi.fn(() => "localhost:3000"),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHostWithProtocol();
+ const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("http://localhost:3000");
});
- test("should return http for localhost without port", async () => {
- const { headers } = await import("next/headers");
+ test("should return http for localhost without port", () => {
const mockHeaders = {
get: vi.fn(() => "localhost"),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHostWithProtocol();
+ const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("http://localhost");
});
- test("should return https for custom domain", async () => {
- const { headers } = await import("next/headers");
+ test("should return https for custom domain", () => {
const mockHeaders = {
get: vi.fn(() => "auth.company.com"),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHostWithProtocol();
+ const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://auth.company.com");
});
});
describe("Real-world scenarios", () => {
- test("should handle Vercel rewrite scenario", async () => {
- const { headers } = await import("next/headers");
+ test("should handle Vercel rewrite scenario", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
// Simulate Vercel rewrite: zitadel.com/login -> login-zitadel-qa.vercel.app
@@ -167,16 +105,13 @@ describe("Host utility functions", () => {
if (key === "host") return "login-zitadel-qa.vercel.app";
return null;
}),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHostWithProtocol();
+ const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://zitadel.com");
});
- test("should handle CloudFlare proxy scenario", async () => {
- const { headers } = await import("next/headers");
+ test("should handle CloudFlare proxy scenario", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-forwarded-host") return "auth.company.com";
@@ -184,114 +119,180 @@ describe("Host utility functions", () => {
if (key === "host") return "cloudflare-worker.workers.dev";
return null;
}),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHost();
+ const result = getPublicHost(mockHeaders);
expect(result).toBe("auth.company.com");
});
- test("should handle development environment", async () => {
- const { headers } = await import("next/headers");
+ test("should handle development environment", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "host") return "localhost:3000";
return null;
}),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHostWithProtocol();
+ const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("http://localhost:3000");
});
- test("should handle staging environment with subdomain", async () => {
- const { headers } = await import("next/headers");
+ test("should handle staging environment with subdomain", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-forwarded-host") return "staging-auth.company.com";
if (key === "host") return "staging-internal.vercel.app";
return null;
}),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHostWithProtocol();
+ const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://staging-auth.company.com");
});
+
+ test("should prioritize x-zitadel-instance-host in multi-tenant scenario", () => {
+ const mockHeaders = {
+ get: vi.fn((key: string) => {
+ if (key === "x-zitadel-instance-host") return "customer.zitadel.cloud";
+ if (key === "x-forwarded-host") return "accounts.company.com";
+ if (key === "host") return "internal.vercel.app";
+ return null;
+ }),
+ } as any;
+
+ const result = getInstanceHost(mockHeaders);
+ expect(result).toBe("customer.zitadel.cloud");
+ });
});
describe("Edge cases", () => {
- test("should handle IPv4 addresses", async () => {
- const { headers } = await import("next/headers");
+ test("should handle IPv4 addresses", () => {
const mockHeaders = {
get: vi.fn(() => "192.168.1.100:3000"),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHostWithProtocol();
+ const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://192.168.1.100:3000");
});
- test("should handle IPv6 addresses", async () => {
- const { headers } = await import("next/headers");
+ test("should handle IPv6 addresses", () => {
const mockHeaders = {
get: vi.fn(() => "[::1]:3000"),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHostWithProtocol();
+ const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://[::1]:3000");
});
- test("should handle hosts with ports", async () => {
- const { headers } = await import("next/headers");
+ test("should handle hosts with ports", () => {
const mockHeaders = {
get: vi.fn(() => "zitadel.com:8080"),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHostWithProtocol();
+ const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://zitadel.com:8080");
});
- test("should handle localhost with different ports", async () => {
- const { headers } = await import("next/headers");
+ test("should handle localhost with different ports", () => {
const mockHeaders = {
get: vi.fn(() => "localhost:8080"),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
-
- const result = await getOriginalHostWithProtocol();
+ const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("http://localhost:8080");
});
+ });
- test("should handle priority order correctly", async () => {
- const { headers } = await import("next/headers");
+ describe("getPublicHost", () => {
+ test("should use x-zitadel-public-host when available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
- // All headers are present, should return x-forwarded-host (highest priority)
- if (key === "x-forwarded-host") return "priority1.com";
- if (key === "x-original-host") return "priority2.com";
- if (key === "host") return "priority3.com";
+ if (key === "x-zitadel-public-host") return "public.zitadel.cloud";
+ if (key === "x-forwarded-host") return "accounts.company.com";
return null;
}),
- };
+ } as any;
- vi.mocked(headers).mockResolvedValue(mockHeaders as any);
+ const result = getPublicHost(mockHeaders);
+ expect(result).toBe("public.zitadel.cloud");
+ expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
+ });
- const result = await getOriginalHost();
- expect(result).toBe("priority1.com");
- // Should only call x-forwarded-host since it's available
+ test("should use x-zitadel-forward-host when x-zitadel-public-host is not available", () => {
+ const mockHeaders = {
+ get: vi.fn((key: string) => {
+ if (key === "x-zitadel-public-host") return null;
+ if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
+ if (key === "x-forwarded-host") return "accounts.company.com";
+ return null;
+ }),
+ } as any;
+
+ const result = getPublicHost(mockHeaders);
+ expect(result).toBe("forward.zitadel.cloud");
+ expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
+ expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
+ });
+
+ test("should use x-forwarded-host when neither x-zitadel-public-host nor x-zitadel-forward-host is available", () => {
+ const mockHeaders = {
+ get: vi.fn((key: string) => {
+ if (key === "x-zitadel-public-host") return null;
+ if (key === "x-zitadel-forward-host") return null;
+ if (key === "x-forwarded-host") return "accounts.company.com";
+ if (key === "host") return "internal.server";
+ return null;
+ }),
+ } as any;
+
+ const result = getPublicHost(mockHeaders);
+ expect(result).toBe("accounts.company.com");
+ expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
+ expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
- expect(mockHeaders.get).toHaveBeenCalledTimes(1);
+ });
+
+ test("should fall back to host when x-forwarded-host is not available", () => {
+ const mockHeaders = {
+ get: vi.fn((key: string) => {
+ if (key === "x-zitadel-public-host") return null;
+ if (key === "x-zitadel-forward-host") return null;
+ if (key === "x-forwarded-host") return null;
+ if (key === "host") return "localhost:3000";
+ return null;
+ }),
+ } as any;
+
+ const result = getPublicHost(mockHeaders);
+ expect(result).toBe("localhost:3000");
+ expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
+ expect(mockHeaders.get).toHaveBeenCalledWith("host");
+ });
+
+ test("should throw error when no host is found", () => {
+ const mockHeaders = {
+ get: vi.fn(() => null),
+ } as any;
+
+ expect(() => getPublicHost(mockHeaders)).toThrow("No host found in headers");
+ });
+
+ test("should differ from getInstanceHost when x-zitadel-instance-host is present", () => {
+ const mockHeaders = {
+ get: vi.fn((key: string) => {
+ if (key === "x-zitadel-instance-host") return "instance.zitadel.cloud";
+ if (key === "x-forwarded-host") return "accounts.company.com";
+ if (key === "host") return "internal.server";
+ return null;
+ }),
+ } as any;
+
+ const instanceHost = getInstanceHost(mockHeaders);
+ const publicHost = getPublicHost(mockHeaders);
+
+ expect(instanceHost).toBe("instance.zitadel.cloud");
+ expect(publicHost).toBe("accounts.company.com");
+ expect(instanceHost).not.toBe(publicHost);
});
});
});
diff --git a/apps/login/src/lib/server/host.ts b/apps/login/src/lib/server/host.ts
index 9d2d57b8a..6c44f8fee 100644
--- a/apps/login/src/lib/server/host.ts
+++ b/apps/login/src/lib/server/host.ts
@@ -1,48 +1,48 @@
-import { headers } from "next/headers";
+import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
/**
* Gets the original host that the user sees in their browser URL.
* When using rewrites this function prioritizes forwarded headers that preserve the original host.
*
- * ⚠️ SERVER-SIDE ONLY: This function can only be used in:
- * - Server Actions (functions with "use server")
- * - Server Components (React components that run on the server)
- * - Route Handlers (API routes)
- * - Middleware
- *
* @returns The host string (e.g., "zitadel.com")
* @throws Error if no host is found
*/
-export async function getOriginalHost(): Promise {
- const _headers = await headers();
+export function getInstanceHost(headers: ReadonlyHeaders): string | null {
+ // use standard proxy headers (x-forwarded-host → host) for both multi-tenant and self-hosted, do not use x-zitadel-instance-host
+ const instanceHost = headers.get("x-zitadel-instance-host") || headers.get("x-zitadel-forward-host");
- // Priority order:
- // 1. x-forwarded-host - Set by proxies/CDNs with the original host
- // 2. x-original-host - Alternative header sometimes used
- // 3. host - Fallback to the current host header
- const host = _headers.get("x-forwarded-host") || _headers.get("x-original-host") || _headers.get("host");
-
- if (!host || typeof host !== "string") {
- throw new Error("No host found in headers");
- }
-
- return host;
+ return instanceHost;
}
/**
- * Gets the original host with protocol prefix.
- * Automatically detects if localhost should use http:// or https://
+ * Gets the public host that the user sees in their browser URL.
+ * Only considers standard proxy headers (x-forwarded-host and host).
+ * Does NOT include x-zitadel-instance-host.
*
- * ⚠️ SERVER-SIDE ONLY: This function can only be used in:
- * - Server Actions (functions with "use server")
- * - Server Components (React components that run on the server)
- * - Route Handlers (API routes)
- * - Middleware
+ * Use this when you need the public-facing host that the user actually sees,
+ * not the internal instance host used for API routing.
*
- * @returns The full URL prefix (e.g., "https://zitadel.com")
+ * @returns The public host string (e.g., "accounts.company.com")
+ * @throws Error if no host is found
*/
-export async function getOriginalHostWithProtocol(): Promise {
- const host = await getOriginalHost();
+export function getPublicHost(headers: ReadonlyHeaders): string {
+ // Only use standard proxy headers (x-zitadel-public-host → x-zitadel-forward-host → x-forwarded-host → host)
+ // Do NOT use x-zitadel-instance-host as it may differ from what the user sees
+ const publicHost =
+ headers.get("x-zitadel-public-host") ||
+ headers.get("x-zitadel-forward-host") ||
+ headers.get("x-forwarded-host") ||
+ headers.get("host");
+
+ if (!publicHost || typeof publicHost !== "string") {
+ throw new Error("No host found in headers");
+ }
+
+ return publicHost;
+}
+
+export function getPublicHostWithProtocol(headers: ReadonlyHeaders): string {
+ const host = getPublicHost(headers);
const protocol = host.includes("localhost") ? "http://" : "https://";
return `${protocol}${host}`;
}
diff --git a/apps/login/src/lib/server/idp-intent.test.ts b/apps/login/src/lib/server/idp-intent.test.ts
index fae6bd432..d2ae229af 100644
--- a/apps/login/src/lib/server/idp-intent.test.ts
+++ b/apps/login/src/lib/server/idp-intent.test.ts
@@ -12,7 +12,7 @@ vi.mock("@zitadel/client", () => ({
}));
vi.mock("../service-url", () => ({
- getServiceUrlFromHeaders: vi.fn(),
+ getServiceConfig: vi.fn(),
}));
vi.mock("../zitadel", () => ({
@@ -111,7 +111,7 @@ describe("processIDPCallback", () => {
// Import mocked modules
const { headers } = await import("next/headers");
- const { getServiceUrlFromHeaders } = await import("../service-url");
+ const { getServiceConfig } = await import("../service-url");
const {
retrieveIDPIntent,
getIDPByID,
@@ -129,7 +129,7 @@ describe("processIDPCallback", () => {
// Setup mocks
mockHeaders = vi.mocked(headers);
- mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
+ mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
mockRetrieveIDPIntent = vi.mocked(retrieveIDPIntent);
mockGetIDPByID = vi.mocked(getIDPByID);
mockUpdateHuman = vi.mocked(updateHuman);
@@ -146,7 +146,7 @@ describe("processIDPCallback", () => {
// Default mock implementations
mockHeaders.mockResolvedValue({} as any);
mockGetServiceUrlFromHeaders.mockReturnValue({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
});
mockRetrieveIDPIntent.mockResolvedValue(defaultIntent);
mockGetIDPByID.mockResolvedValue(defaultIdp);
@@ -257,7 +257,7 @@ describe("processIDPCallback", () => {
const result = await processIDPCallback(defaultParams);
expect(mockRetrieveIDPIntent).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
id: "intent123",
token: "token123",
});
@@ -287,7 +287,7 @@ describe("processIDPCallback", () => {
await processIDPCallback(defaultParams);
expect(mockUpdateHuman).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
request: expect.objectContaining({
userId: "user123",
profile: defaultIntent.updateHumanUser.profile,
@@ -353,7 +353,7 @@ describe("processIDPCallback", () => {
const result = await processIDPCallback(linkParams);
expect(mockAddIDPLink).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
idp: {
id: "idp123",
userId: "user123",
@@ -442,12 +442,12 @@ describe("processIDPCallback", () => {
const result = await processIDPCallback(defaultParams);
expect(mockListUsers).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
email: "test@example.com",
organizationId: "org123",
});
expect(mockAddIDPLink).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
idp: {
id: "idp123",
userId: "user123",
@@ -523,7 +523,7 @@ describe("processIDPCallback", () => {
const result = await processIDPCallback(defaultParams);
expect(mockListUsers).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
userName: "testuser",
organizationId: "org123",
});
@@ -557,7 +557,7 @@ describe("processIDPCallback", () => {
const result = await processIDPCallback(defaultParams);
expect(mockAddHuman).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
request: expect.objectContaining({
username: "testuser",
profile: defaultIntent.addHumanUser.profile,
@@ -602,11 +602,11 @@ describe("processIDPCallback", () => {
});
expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
domain: "example.com",
});
expect(mockAddHuman).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
request: expect.objectContaining({
organization: expect.objectContaining({
org: { case: "orgId", value: "org-from-domain" },
@@ -625,10 +625,10 @@ describe("processIDPCallback", () => {
});
expect(mockGetDefaultOrg).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
});
expect(mockAddHuman).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
request: expect.objectContaining({
organization: expect.objectContaining({
org: { case: "orgId", value: "default-org" },
@@ -901,14 +901,14 @@ describe("validateIDPLinkingPermissions", () => {
});
const result = await validateIDPLinkingPermissions({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
expect(result).toBe(false);
expect(mockGetLoginSettings).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
organization: "org123",
});
expect(mockGetActiveIdentityProviders).not.toHaveBeenCalled();
@@ -918,7 +918,7 @@ describe("validateIDPLinkingPermissions", () => {
mockGetLoginSettings.mockResolvedValue(undefined);
const result = await validateIDPLinkingPermissions({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
@@ -930,7 +930,7 @@ describe("validateIDPLinkingPermissions", () => {
mockGetLoginSettings.mockResolvedValue({});
const result = await validateIDPLinkingPermissions({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
@@ -952,14 +952,14 @@ describe("validateIDPLinkingPermissions", () => {
});
const result = await validateIDPLinkingPermissions({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
expect(result).toBe(false);
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
orgId: "org123",
linking_allowed: true,
});
@@ -974,7 +974,7 @@ describe("validateIDPLinkingPermissions", () => {
});
const result = await validateIDPLinkingPermissions({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
@@ -989,7 +989,7 @@ describe("validateIDPLinkingPermissions", () => {
mockGetActiveIdentityProviders.mockResolvedValue({});
const result = await validateIDPLinkingPermissions({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
@@ -1011,18 +1011,18 @@ describe("validateIDPLinkingPermissions", () => {
});
const result = await validateIDPLinkingPermissions({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
expect(result).toBe(true);
expect(mockGetLoginSettings).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
organization: "org123",
});
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
orgId: "org123",
linking_allowed: true,
});
@@ -1042,7 +1042,7 @@ describe("validateIDPLinkingPermissions", () => {
});
const result = await validateIDPLinkingPermissions({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
@@ -1057,7 +1057,7 @@ describe("validateIDPLinkingPermissions", () => {
await expect(
validateIDPLinkingPermissions({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
}),
@@ -1072,7 +1072,7 @@ describe("validateIDPLinkingPermissions", () => {
await expect(
validateIDPLinkingPermissions({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
}),
@@ -1088,7 +1088,7 @@ describe("validateIDPLinkingPermissions", () => {
});
const result = await validateIDPLinkingPermissions({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
diff --git a/apps/login/src/lib/server/idp-intent.ts b/apps/login/src/lib/server/idp-intent.ts
index 02eb82d0a..9ab3372ca 100644
--- a/apps/login/src/lib/server/idp-intent.ts
+++ b/apps/login/src/lib/server/idp-intent.ts
@@ -1,6 +1,6 @@
"use server";
-import { getServiceUrlFromHeaders } from "@/lib/service-url";
+import { getServiceConfig } from "@/lib/service-url";
import {
retrieveIDPIntent,
getIDPByID,
@@ -13,6 +13,7 @@ import {
getActiveIdentityProviders,
getUserByID,
getDefaultOrg,
+ ServiceConfig,
} from "@/lib/zitadel";
import { headers } from "next/headers";
import { create } from "@zitadel/client";
@@ -31,11 +32,11 @@ const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
async function resolveOrganizationForUser({
organization,
addHumanUser,
- serviceUrl,
+ serviceConfig,
}: {
organization?: string;
addHumanUser?: AddHumanUserRequest;
- serviceUrl: string;
+ serviceConfig: ServiceConfig;
}): Promise {
if (organization) return organization;
@@ -43,16 +44,12 @@ async function resolveOrganizationForUser({
const matched = ORG_SUFFIX_REGEX.exec(addHumanUser.username);
const suffix = matched?.[1] ?? "";
- const orgs = await getOrgsByDomain({
- serviceUrl,
- domain: suffix,
+ const orgs = await getOrgsByDomain({ serviceConfig, domain: suffix,
});
const orgToCheckForDiscovery = orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
if (orgToCheckForDiscovery) {
- const orgLoginSettings = await getLoginSettings({
- serviceUrl,
- organization: orgToCheckForDiscovery,
+ const orgLoginSettings = await getLoginSettings({ serviceConfig, organization: orgToCheckForDiscovery,
});
if (orgLoginSettings?.allowDomainDiscovery) {
return orgToCheckForDiscovery;
@@ -61,7 +58,7 @@ async function resolveOrganizationForUser({
}
// Fallback to default organization if no org was resolved through discovery
- const defaultOrg = await getDefaultOrg({ serviceUrl });
+ const defaultOrg = await getDefaultOrg({ serviceConfig });
return defaultOrg?.id;
}
@@ -72,19 +69,15 @@ async function resolveOrganizationForUser({
* 2. The specific IDP is activated for the organization
*
*/
-export async function validateIDPLinkingPermissions({
- serviceUrl,
- userOrganizationId,
+export async function validateIDPLinkingPermissions({ serviceConfig, userOrganizationId,
idpId,
}: {
- serviceUrl: string;
+ serviceConfig: ServiceConfig;
userOrganizationId: string;
idpId: string;
}): Promise {
// Check organization login settings
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization: userOrganizationId,
+ const loginSettings = await getLoginSettings({ serviceConfig, organization: userOrganizationId,
});
if (!loginSettings?.allowExternalIdp) {
@@ -92,9 +85,7 @@ export async function validateIDPLinkingPermissions({
}
// Check if the IDP is activated for the organization and allows linking
- const activeIDPs = await getActiveIdentityProviders({
- serviceUrl,
- orgId: userOrganizationId,
+ const activeIDPs = await getActiveIdentityProviders({ serviceConfig, orgId: userOrganizationId,
linking_allowed: true,
});
@@ -132,7 +123,7 @@ export async function processIDPCallback({
postErrorRedirectUrl?: string;
}): Promise<{ redirect?: string; error?: string }> {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("idp");
@@ -155,9 +146,7 @@ export async function processIDPCallback({
});
// Consume the single-use token ONCE
- const intent = await retrieveIDPIntent({
- serviceUrl,
- id,
+ const intent = await retrieveIDPIntent({ serviceConfig, id,
token,
});
@@ -171,9 +160,7 @@ export async function processIDPCallback({
}
// Get IDP configuration
- const idp = await getIDPByID({
- serviceUrl,
- id: idpInformation.idpId,
+ const idp = await getIDPByID({ serviceConfig, id: idpInformation.idpId,
});
if (!idp) {
@@ -207,9 +194,7 @@ export async function processIDPCallback({
// Auto-update user if enabled
if (options?.isAutoUpdate && updateHumanUser) {
try {
- await updateHuman({
- serviceUrl,
- request: create(UpdateHumanUserRequestSchema, {
+ await updateHuman({ serviceConfig, request: create(UpdateHumanUserRequestSchema, {
userId: userId,
profile: updateHumanUser.profile,
email: updateHumanUser.email,
@@ -259,7 +244,7 @@ export async function processIDPCallback({
try {
// Get user to retrieve their organization
- const targetUser = await getUserByID({ serviceUrl, userId });
+ const targetUser = await getUserByID({ serviceConfig, userId });
if (!targetUser || !targetUser.details?.resourceOwner) {
console.error("[IDP Process] User not found or missing organization");
@@ -268,9 +253,7 @@ export async function processIDPCallback({
}
// Validate IDP linking permissions
- const isAllowed = await validateIDPLinkingPermissions({
- serviceUrl,
- userOrganizationId: targetUser.details.resourceOwner,
+ const isAllowed = await validateIDPLinkingPermissions({ serviceConfig, userOrganizationId: targetUser.details.resourceOwner,
idpId: idpInformation.idpId,
});
@@ -280,9 +263,7 @@ export async function processIDPCallback({
return { redirect: `/idp/${provider}/linking-failed?${params}&error=validation_failed` };
}
- await addIDPLink({
- serviceUrl,
- idp: {
+ await addIDPLink({ serviceConfig, idp: {
id: idpInformation.idpId,
userId: idpInformation.userId,
userName: idpInformation.userName,
@@ -328,21 +309,17 @@ export async function processIDPCallback({
const email = addHumanUser?.email?.email;
if (options.autoLinking === AutoLinkingOption.EMAIL && email) {
- foundUser = await listUsers({ serviceUrl, email, organizationId: organization }).then((response) => {
+ foundUser = await listUsers({ serviceConfig, email, organizationId: organization }).then((response) => {
return response.result ? response.result[0] : null;
});
} else if (options.autoLinking === AutoLinkingOption.USERNAME) {
- foundUser = await listUsers({
- serviceUrl,
- userName: idpInformation.userName,
+ foundUser = await listUsers({ serviceConfig, userName: idpInformation.userName,
organizationId: organization,
}).then((response) => {
return response.result ? response.result[0] : null;
});
} else {
- foundUser = await listUsers({
- serviceUrl,
- userName: idpInformation.userName,
+ foundUser = await listUsers({ serviceConfig, userName: idpInformation.userName,
email,
organizationId: organization,
}).then((response) => {
@@ -359,9 +336,7 @@ export async function processIDPCallback({
}
// Validate IDP linking permissions
- const isAllowed = await validateIDPLinkingPermissions({
- serviceUrl,
- userOrganizationId: foundUser.details.resourceOwner,
+ const isAllowed = await validateIDPLinkingPermissions({ serviceConfig, userOrganizationId: foundUser.details.resourceOwner,
idpId: idpInformation.idpId,
});
@@ -371,9 +346,7 @@ export async function processIDPCallback({
return { redirect: `/idp/${provider}/linking-failed?${params}&error=validation_failed` };
}
- await addIDPLink({
- serviceUrl,
- idp: {
+ await addIDPLink({ serviceConfig, idp: {
id: idpInformation.idpId,
userId: idpInformation.userId,
userName: idpInformation.userName,
@@ -419,7 +392,7 @@ export async function processIDPCallback({
const orgToRegisterOn = await resolveOrganizationForUser({
organization,
addHumanUser,
- serviceUrl,
+ serviceConfig,
});
if (!orgToRegisterOn) {
@@ -438,9 +411,7 @@ export async function processIDPCallback({
});
try {
- const newUser = await addHuman({
- serviceUrl,
- request: addHumanUserWithOrganization,
+ const newUser = await addHuman({ serviceConfig, request: addHumanUserWithOrganization,
});
console.log("[IDP Process] User auto-created successfully, creating session");
@@ -480,7 +451,7 @@ export async function processIDPCallback({
const orgToRegisterOn = await resolveOrganizationForUser({
organization,
addHumanUser,
- serviceUrl,
+ serviceConfig,
});
if (!orgToRegisterOn) {
diff --git a/apps/login/src/lib/server/idp.test.ts b/apps/login/src/lib/server/idp.test.ts
index 68025988e..693c828ab 100644
--- a/apps/login/src/lib/server/idp.test.ts
+++ b/apps/login/src/lib/server/idp.test.ts
@@ -13,11 +13,12 @@ vi.mock("next/navigation", () => ({
}));
vi.mock("../service-url", () => ({
- getServiceUrlFromHeaders: vi.fn(),
+ getServiceConfig: vi.fn(),
}));
vi.mock("./host", () => ({
- getOriginalHost: vi.fn(),
+ getInstanceHost: vi.fn(),
+ getPublicHost: vi.fn(),
}));
vi.mock("../zitadel", () => ({
@@ -27,7 +28,8 @@ vi.mock("../zitadel", () => ({
describe("redirectToIdp", () => {
let mockHeaders: any;
let mockGetServiceUrlFromHeaders: any;
- let mockGetOriginalHost: any;
+ let mockGetInstanceHost: any;
+ let mockGetPublicHost: any;
let mockStartIdentityProviderFlow: any;
beforeEach(async () => {
@@ -35,22 +37,24 @@ describe("redirectToIdp", () => {
// Import mocked modules
const { headers } = await import("next/headers");
- const { getServiceUrlFromHeaders } = await import("../service-url");
- const { getOriginalHost } = await import("./host");
+ const { getServiceConfig } = await import("../service-url");
+ const { getInstanceHost, getPublicHost } = await import("./host");
const { startIdentityProviderFlow } = await import("../zitadel");
// Setup mocks
mockHeaders = vi.mocked(headers);
- mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
- mockGetOriginalHost = vi.mocked(getOriginalHost);
+ mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
+ mockGetInstanceHost = vi.mocked(getInstanceHost);
+ mockGetPublicHost = vi.mocked(getPublicHost);
mockStartIdentityProviderFlow = vi.mocked(startIdentityProviderFlow);
// Default mock implementations
mockHeaders.mockResolvedValue({} as any);
mockGetServiceUrlFromHeaders.mockReturnValue({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
});
- mockGetOriginalHost.mockResolvedValue("example.com");
+ mockGetInstanceHost.mockReturnValue("example.com");
+ mockGetPublicHost.mockReturnValue("example.com");
});
afterEach(() => {
@@ -76,7 +80,7 @@ describe("redirectToIdp", () => {
}
expect(mockStartIdentityProviderFlow).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
idpId: "idp123",
urls: {
successUrl: expect.stringContaining("postErrorRedirectUrl=https%3A%2F%2Fapp.example.com%2Ferror"),
@@ -115,7 +119,7 @@ describe("redirectToIdp", () => {
}
expect(mockStartIdentityProviderFlow).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
idpId: "idp123",
urls: {
successUrl: expect.not.stringContaining("postErrorRedirectUrl"),
diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts
index f38adcb34..711522221 100644
--- a/apps/login/src/lib/server/idp.ts
+++ b/apps/login/src/lib/server/idp.ts
@@ -6,21 +6,22 @@ import {
listAuthenticationMethodTypes,
startIdentityProviderFlow,
startLDAPIdentityProviderFlow,
+ ServiceConfig,
} from "@/lib/zitadel";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { completeFlowOrGetUrl } from "../client";
-import { getServiceUrlFromHeaders } from "../service-url";
+import { getServiceConfig } from "../service-url";
import { checkEmailVerification, checkMFAFactors } from "../verify-helper";
import { createSessionForIdpAndUpdateCookie } from "./cookie";
-import { getOriginalHost } from "./host";
+import { getPublicHost } from "./host";
export type RedirectToIdpState = { error?: string | null } | undefined;
export async function redirectToIdp(prevState: RedirectToIdpState, formData: FormData): Promise {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
- const host = await getOriginalHost();
+ const { serviceConfig } = getServiceConfig(_headers);
+ const host = getPublicHost(_headers);
const params = new URLSearchParams();
@@ -43,7 +44,7 @@ export async function redirectToIdp(prevState: RedirectToIdpState, formData: For
}
const response = await startIDPFlow({
- serviceUrl,
+ serviceConfig,
host,
idpId,
successUrl: `/idp/${provider}/process?` + params.toString(),
@@ -62,7 +63,7 @@ export async function redirectToIdp(prevState: RedirectToIdpState, formData: For
}
export type StartIDPFlowCommand = {
- serviceUrl: string;
+ serviceConfig: ServiceConfig;
host: string;
idpId: string;
successUrl: string;
@@ -73,7 +74,7 @@ async function startIDPFlow(command: StartIDPFlowCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const url = await startIdentityProviderFlow({
- serviceUrl: command.serviceUrl,
+ serviceConfig: command.serviceConfig,
idpId: command.idpId,
urls: {
successUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.successUrl}`,
@@ -103,25 +104,19 @@ export type CreateNewSessionCommand = {
export async function createNewSessionFromIdpIntent(command: CreateNewSessionCommand) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
if (!command.userId || !command.idpIntent) {
throw new Error("No userId or loginName provided");
}
- const userResponse = await getUserByID({
- serviceUrl,
- userId: command.userId,
- });
+ const userResponse = await getUserByID({ serviceConfig, userId: command.userId });
if (!userResponse || !userResponse.user) {
return { error: "User not found in the system" };
}
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization: userResponse.user.details?.resourceOwner,
- });
+ const loginSettings = await getLoginSettings({ serviceConfig, organization: userResponse.user.details?.resourceOwner });
const session = await createSessionForIdpAndUpdateCookie({
userId: command.userId,
@@ -146,17 +141,14 @@ export async function createNewSessionFromIdpIntent(command: CreateNewSessionCom
// check if user has MFA methods
let authMethods;
if (session.factors?.user?.id) {
- const response = await listAuthenticationMethodTypes({
- serviceUrl,
- userId: session.factors.user.id,
- });
+ const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
if (response.authMethodTypes && response.authMethodTypes.length) {
authMethods = response.authMethodTypes;
}
}
const mfaFactorCheck = await checkMFAFactors(
- serviceUrl,
+ serviceConfig,
session,
loginSettings,
authMethods || [], // Pass empty array if no auth methods
@@ -193,14 +185,14 @@ type createNewSessionForLDAPCommand = {
export async function createNewSessionForLDAP(command: createNewSessionForLDAPCommand) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
if (!command.username || !command.password) {
return { error: "No username or password provided" };
}
const response = await startLDAPIdentityProviderFlow({
- serviceUrl,
+ serviceConfig,
idpId: command.idpId,
username: command.username,
password: command.password,
diff --git a/apps/login/src/lib/server/loginname.test.ts b/apps/login/src/lib/server/loginname.test.ts
index 5fcab5122..2f3f3d1bf 100644
--- a/apps/login/src/lib/server/loginname.test.ts
+++ b/apps/login/src/lib/server/loginname.test.ts
@@ -15,7 +15,7 @@ vi.mock("@zitadel/client", () => ({
}));
vi.mock("../service-url", () => ({
- getServiceUrlFromHeaders: vi.fn(),
+ getServiceConfig: vi.fn(),
}));
vi.mock("../idp", () => ({
@@ -39,7 +39,8 @@ vi.mock("./cookie", () => ({
}));
vi.mock("./host", () => ({
- getOriginalHost: vi.fn(),
+ getInstanceHost: vi.fn(),
+ getPublicHost: vi.fn(),
}));
// this returns the key itself that can be checked not the translated value
@@ -57,7 +58,8 @@ describe("sendLoginname", () => {
let mockCreateSessionAndUpdateCookie: any;
let mockListAuthenticationMethodTypes: any;
let mockListIDPLinks: any;
- let mockGetOriginalHost: any;
+ let mockGetInstanceHost: any;
+ let mockGetPublicHost: any;
let mockStartIdentityProviderFlow: any;
let mockGetActiveIdentityProviders: any;
let mockGetIDPByID: any;
@@ -70,7 +72,7 @@ describe("sendLoginname", () => {
// Import mocked modules
const { headers } = await import("next/headers");
const { create } = await import("@zitadel/client");
- const { getServiceUrlFromHeaders } = await import("../service-url");
+ const { getServiceConfig } = await import("../service-url");
const {
getLoginSettings,
searchUsers,
@@ -81,19 +83,20 @@ describe("sendLoginname", () => {
getOrgsByDomain,
} = await import("../zitadel");
const { createSessionAndUpdateCookie } = await import("./cookie");
- const { getOriginalHost } = await import("./host");
+ const { getInstanceHost, getPublicHost } = await import("./host");
const { idpTypeToSlug } = await import("../idp");
// Setup mocks
mockHeaders = vi.mocked(headers);
mockCreate = vi.mocked(create);
- mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
+ mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
mockGetLoginSettings = vi.mocked(getLoginSettings);
mockSearchUsers = vi.mocked(searchUsers);
mockCreateSessionAndUpdateCookie = vi.mocked(createSessionAndUpdateCookie);
mockListAuthenticationMethodTypes = vi.mocked(listAuthenticationMethodTypes);
mockListIDPLinks = vi.mocked(listIDPLinks);
- mockGetOriginalHost = vi.mocked(getOriginalHost);
+ mockGetInstanceHost = vi.mocked(getInstanceHost);
+ mockGetPublicHost = vi.mocked(getPublicHost);
mockStartIdentityProviderFlow = vi.mocked(startIdentityProviderFlow);
mockGetActiveIdentityProviders = vi.mocked(getActiveIdentityProviders);
mockGetIDPByID = vi.mocked(getIDPByID);
@@ -102,8 +105,9 @@ describe("sendLoginname", () => {
// Default mock implementations
mockHeaders.mockResolvedValue({} as any);
- mockGetServiceUrlFromHeaders.mockReturnValue({ serviceUrl: "https://api.example.com" });
- mockGetOriginalHost.mockResolvedValue("example.com");
+ mockGetServiceUrlFromHeaders.mockReturnValue({ serviceConfig: { baseUrl: "https://api.example.com" } });
+ mockGetInstanceHost.mockReturnValue("example.com");
+ mockGetPublicHost.mockReturnValue("example.com");
mockIdpTypeToSlug.mockReturnValue("google");
mockGetIDPByID.mockResolvedValue({
id: "idp123",
@@ -243,7 +247,7 @@ describe("sendLoginname", () => {
expect(result).toEqual({ redirect: "https://idp.example.com/auth" });
expect(mockListIDPLinks).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
userId: "user123",
});
});
@@ -283,7 +287,7 @@ describe("sendLoginname", () => {
expect(result).toEqual({ redirect: "https://org-idp.example.com/auth" });
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
orgId: "org123", // User's organization from resourceOwner
});
});
@@ -523,7 +527,7 @@ describe("sendLoginname", () => {
// Verify org discovery was called with correct domain
expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
domain: "example.com",
});
});
@@ -562,13 +566,13 @@ describe("sendLoginname", () => {
// Verify org discovery was called
expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
domain: "company.com",
});
// Verify IDP redirect was called with discovered org
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
- serviceUrl: "https://api.example.com",
+ serviceConfig: { baseUrl: "https://api.example.com" },
orgId: "discovered-org-456",
});
});
diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts
index e09dd34b1..862d71372 100644
--- a/apps/login/src/lib/server/loginname.ts
+++ b/apps/login/src/lib/server/loginname.ts
@@ -9,7 +9,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
-import { getServiceUrlFromHeaders } from "../service-url";
+import { getServiceConfig } from "../service-url";
import {
getActiveIdentityProviders,
getIDPByID,
@@ -22,7 +22,7 @@ import {
startIdentityProviderFlow,
} from "../zitadel";
import { createSessionAndUpdateCookie } from "./cookie";
-import { getOriginalHost } from "./host";
+import { getPublicHost } from "./host";
import { IDPLink } from "@zitadel/proto/zitadel/user/v2/idp_pb";
export type SendLoginnameCommand = {
@@ -36,21 +36,18 @@ const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
export async function sendLoginname(command: SendLoginnameCommand) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("loginname");
- const loginSettingsByContext = await getLoginSettings({
- serviceUrl,
- organization: command.organization,
- });
+ const loginSettingsByContext = await getLoginSettings({ serviceConfig, organization: command.organization });
if (!loginSettingsByContext) {
return { error: t("errors.couldNotGetLoginSettings") };
}
let searchUsersRequest: SearchUsersCommand = {
- serviceUrl,
+ serviceConfig,
searchValue: command.loginName,
organizationId: command.organization,
loginSettings: loginSettingsByContext,
@@ -88,28 +85,22 @@ export async function sendLoginname(command: SendLoginnameCommand) {
// If userId is provided, check for user-specific IDP links first
let identityProviders: IDPLink[] = [];
if (userId) {
- identityProviders = await listIDPLinks({
- serviceUrl,
- userId,
- }).then((resp) => {
+ identityProviders = await listIDPLinks({ serviceConfig, userId }).then((resp) => {
return resp.result;
});
}
// If no IDP links exist for the user (or no userId provided), try to get active IDPs from the organization
if (identityProviders.length === 0) {
- const activeIdps = await getActiveIdentityProviders({
- serviceUrl,
- orgId: organization,
- }).then((resp) => {
+ const activeIdps = await getActiveIdentityProviders({ serviceConfig, orgId: organization }).then((resp) => {
return resp.identityProviders;
});
// If exactly one active IDP exists in the organization, redirect to it
if (activeIdps.length === 1) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
- const host = await getOriginalHost();
+ const { serviceConfig } = getServiceConfig(_headers);
+ const host = getPublicHost(_headers);
const identityProviderType = activeIdps[0].type;
const provider = idpTypeToSlug(identityProviderType);
@@ -131,7 +122,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const url = await startIdentityProviderFlow({
- serviceUrl,
+ serviceConfig,
idpId: activeIdps[0].id,
urls: {
successUrl:
@@ -153,15 +144,12 @@ export async function sendLoginname(command: SendLoginnameCommand) {
if (identityProviders.length === 1) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
- const host = await getOriginalHost();
+ const { serviceConfig } = getServiceConfig(_headers);
+ const host = getPublicHost(_headers);
const identityProviderId = identityProviders[0].idpId;
- const idp = await getIDPByID({
- serviceUrl,
- id: identityProviderId,
- });
+ const idp = await getIDPByID({ serviceConfig, id: identityProviderId });
const idpType = idp?.type;
@@ -189,7 +177,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const url = await startIdentityProviderFlow({
- serviceUrl,
+ serviceConfig,
idpId: idp.id,
urls: {
successUrl:
@@ -216,10 +204,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const user = users[0];
const userId = users[0].userId;
- const userLoginSettings = await getLoginSettings({
- serviceUrl,
- organization: user.details?.resourceOwner,
- });
+ const userLoginSettings = await getLoginSettings({ serviceConfig, organization: user.details?.resourceOwner });
// compare with the concatenated suffix when set
const concatLoginname = command.suffix ? `${command.loginName}@${command.suffix}` : command.loginName;
@@ -273,10 +258,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
// Resolve organization from command or session
const organization = command.organization ?? session.factors?.user?.organizationId;
- const methods = await listAuthenticationMethodTypes({
- serviceUrl,
- userId: session.factors?.user?.id,
- });
+ const methods = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors?.user?.id });
// always resend invite if user has no auth method set
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
@@ -418,18 +400,12 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const suffix = matched?.[1] ?? "";
// this just returns orgs where the suffix is set as primary domain
- const orgs = await getOrgsByDomain({
- serviceUrl,
- domain: suffix,
- });
+ const orgs = await getOrgsByDomain({ serviceConfig, domain: suffix });
const orgToCheckForDiscovery = orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
if (orgToCheckForDiscovery) {
- const orgLoginSettings = await getLoginSettings({
- serviceUrl,
- organization: orgToCheckForDiscovery,
- });
+ const orgLoginSettings = await getLoginSettings({ serviceConfig, organization: orgToCheckForDiscovery });
if (orgLoginSettings?.allowDomainDiscovery) {
console.log("org discovery successful, using org:", orgToCheckForDiscovery);
diff --git a/apps/login/src/lib/server/oidc.ts b/apps/login/src/lib/server/oidc.ts
index 36a31fe41..5fbf1608b 100644
--- a/apps/login/src/lib/server/oidc.ts
+++ b/apps/login/src/lib/server/oidc.ts
@@ -2,14 +2,12 @@
import { getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest } from "@/lib/zitadel";
import { headers } from "next/headers";
-import { getServiceUrlFromHeaders } from "../service-url";
+import { getServiceConfig } from "../service-url";
export async function getDeviceAuthorizationRequest(userCode: string) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
- return zitadelGetDeviceAuthorizationRequest({
- serviceUrl,
- userCode,
+ return zitadelGetDeviceAuthorizationRequest({ serviceConfig, userCode,
});
}
diff --git a/apps/login/src/lib/server/passkeys.test.ts b/apps/login/src/lib/server/passkeys.test.ts
index b22d20479..5cc59c725 100644
--- a/apps/login/src/lib/server/passkeys.test.ts
+++ b/apps/login/src/lib/server/passkeys.test.ts
@@ -14,7 +14,7 @@ vi.mock("@zitadel/client", () => ({
}));
vi.mock("../service-url", () => ({
- getServiceUrlFromHeaders: vi.fn(),
+ getServiceConfig: vi.fn(),
}));
vi.mock("../zitadel", () => ({
@@ -62,7 +62,7 @@ describe("sendPasskey", () => {
// Import mocked modules
const { headers } = await import("next/headers");
- const { getServiceUrlFromHeaders } = await import("../service-url");
+ const { getServiceConfig } = await import("../service-url");
const { getLoginSettings, getUserByID } = await import("../zitadel");
const { setSessionAndUpdateCookie } = await import("./cookie");
const { getSessionCookieById, getSessionCookieByLoginName, getMostRecentSessionCookie } = await import("../cookies");
@@ -71,7 +71,7 @@ describe("sendPasskey", () => {
// Setup mocks
mockHeaders = vi.mocked(headers);
- mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
+ mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
mockGetLoginSettings = vi.mocked(getLoginSettings);
mockGetUserByID = vi.mocked(getUserByID);
mockSetSessionAndUpdateCookie = vi.mocked(setSessionAndUpdateCookie);
diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts
index b6fc46c7a..f53c93caf 100644
--- a/apps/login/src/lib/server/passkeys.ts
+++ b/apps/login/src/lib/server/passkeys.ts
@@ -20,10 +20,10 @@ import { headers } from "next/headers";
import { userAgent } from "next/server";
import { getTranslations } from "next-intl/server";
import { getMostRecentSessionCookie, getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
-import { getServiceUrlFromHeaders } from "../service-url";
+import { getServiceConfig } from "../service-url";
import { checkEmailVerification, checkUserVerification } from "../verify-helper";
import { createSessionAndUpdateCookie, setSessionAndUpdateCookie } from "./cookie";
-import { getOriginalHost } from "./host";
+import { getPublicHost } from "./host";
import { completeFlowOrGetUrl } from "../client";
type VerifyPasskeyCommand = {
@@ -63,8 +63,8 @@ export async function registerPasskeyLink(
}
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
- const host = await getOriginalHost();
+ const { serviceConfig } = getServiceConfig(_headers);
+ const host = getPublicHost(_headers);
let session: GetSessionResponse | undefined;
let createdSession: Session | undefined;
@@ -74,11 +74,7 @@ export async function registerPasskeyLink(
if (command.sessionId) {
// Session-based flow (existing logic)
const sessionCookie = await getSessionCookieById({ sessionId: command.sessionId });
- session = await getSession({
- serviceUrl,
- sessionId: sessionCookie.id,
- sessionToken: sessionCookie.token,
- });
+ session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
if (!session?.session?.factors?.user?.id) {
return { error: "Could not determine user from session" };
@@ -89,10 +85,7 @@ export async function registerPasskeyLink(
const sessionValid = isSessionValid(session.session);
if (!sessionValid.valid) {
- const authmethods = await listAuthenticationMethodTypes({
- serviceUrl,
- userId: currentUserId,
- });
+ const authmethods = await listAuthenticationMethodTypes({ serviceConfig, userId: currentUserId });
// if the user has no authmethods set, we need to check if the user was verified
if (authmethods.authMethodTypes.length !== 0) {
@@ -117,10 +110,7 @@ export async function registerPasskeyLink(
code: command.code,
};
} else {
- const codeResponse = await createPasskeyRegistrationLink({
- serviceUrl,
- userId: currentUserId,
- });
+ const codeResponse = await createPasskeyRegistrationLink({ serviceConfig, userId: currentUserId });
if (!codeResponse?.code?.code) {
return { error: "Could not create registration link" };
@@ -136,10 +126,7 @@ export async function registerPasskeyLink(
};
// Check if user exists
- const userResponse = await getUserByID({
- serviceUrl,
- userId: currentUserId,
- });
+ const userResponse = await getUserByID({ serviceConfig, userId: currentUserId });
if (!userResponse || !userResponse.user) {
return { error: "User not found" };
@@ -179,17 +166,12 @@ export async function registerPasskeyLink(
throw new Error("Could not determine user");
}
- return registerPasskey({
- serviceUrl,
- userId: currentUserId,
- code: registerCode,
- domain: hostname,
- });
+ return registerPasskey({ serviceConfig, userId: currentUserId, code: registerCode, domain: hostname });
}
export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
if (!command.sessionId && !command.userId) {
throw new Error("Either sessionId or userId must be provided");
@@ -214,11 +196,7 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
const sessionCookie = await getSessionCookieById({
sessionId: command.sessionId,
});
- const session = await getSession({
- serviceUrl,
- sessionId: sessionCookie.id,
- sessionToken: sessionCookie.token,
- });
+ const session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
const userId = session?.session?.factors?.user?.id;
if (!userId) {
@@ -231,10 +209,7 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
currentUserId = command.userId!;
// Verify user exists
- const userResponse = await getUserByID({
- serviceUrl,
- userId: currentUserId,
- });
+ const userResponse = await getUserByID({ serviceConfig, userId: currentUserId });
if (!userResponse || !userResponse.user) {
throw new Error("User not found");
@@ -242,7 +217,7 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
}
return zitadelVerifyPasskeyRegistration({
- serviceUrl,
+ serviceConfig,
request: create(VerifyPasskeyRegistrationRequestSchema, {
passkeyId: command.passkeyId,
publicKeyCredential: command.publicKeyCredential,
@@ -279,12 +254,9 @@ export async function sendPasskey(command: SendPasskeyCommand) {
}
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization,
- });
+ const loginSettings = await getLoginSettings({ serviceConfig, organization });
let lifetime = command.lifetime; // Use provided lifetime first
@@ -318,10 +290,7 @@ export async function sendPasskey(command: SendPasskeyCommand) {
let userResponse;
try {
- userResponse = await getUserByID({
- serviceUrl,
- userId: session?.factors?.user?.id,
- });
+ userResponse = await getUserByID({ serviceConfig, userId: session?.factors?.user?.id });
} catch (error) {
console.error("Error fetching user by ID:", error);
return { error: t("verify.errors.couldNotGetUser") };
diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts
index 12adc81ae..f094e2616 100644
--- a/apps/login/src/lib/server/password.ts
+++ b/apps/login/src/lib/server/password.ts
@@ -10,6 +10,7 @@ import {
listAuthenticationMethodTypes,
listUsers,
passwordReset,
+ ServiceConfig,
setPassword,
setUserPassword,
} from "@/lib/zitadel";
@@ -22,8 +23,8 @@ import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_se
import { headers } from "next/headers";
import { completeFlowOrGetUrl } from "../client";
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
-import { getServiceUrlFromHeaders } from "../service-url";
-import { getOriginalHostWithProtocol } from "./host";
+import { getServiceConfig } from "../service-url";
+import { getPublicHostWithProtocol } from "./host";
import {
checkEmailVerification,
checkMFAFactors,
@@ -41,18 +42,14 @@ type ResetPasswordCommand = {
export async function resetPassword(command: ResetPasswordCommand) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("password");
// Get the original host that the user sees with protocol
- const hostWithProtocol = await getOriginalHostWithProtocol();
+ const hostWithProtocol = await getPublicHostWithProtocol(_headers);
- const users = await listUsers({
- serviceUrl,
- loginName: command.loginName,
- organizationId: command.organization,
- });
+ const users = await listUsers({ serviceConfig, loginName: command.loginName, organizationId: command.organization });
if (!users.details || users.details.totalResult !== BigInt(1) || !users.result[0].userId) {
return { error: t("errors.couldNotSendResetLink") };
@@ -62,7 +59,7 @@ export async function resetPassword(command: ResetPasswordCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
return passwordReset({
- serviceUrl,
+ serviceConfig,
userId,
urlTemplate:
`${hostWithProtocol}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
@@ -79,7 +76,7 @@ export type UpdateSessionCommand = {
export async function sendPassword(command: UpdateSessionCommand): Promise<{ error: string } | { redirect: string }> {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("password");
let sessionCookie = await getSessionCookieByLoginName({
@@ -94,11 +91,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
let loginSettings: LoginSettings | undefined;
if (!sessionCookie) {
- const users = await listUsers({
- serviceUrl,
- loginName: command.loginName,
- organizationId: command.organization,
- });
+ const users = await listUsers({ serviceConfig, loginName: command.loginName, organizationId: command.organization });
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
user = users.result[0];
@@ -108,10 +101,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
password: { password: command.checks.password?.password },
});
- loginSettings = await getLoginSettings({
- serviceUrl,
- organization: command.organization,
- });
+ loginSettings = await getLoginSettings({ serviceConfig, organization: command.organization });
try {
session = await createSessionAndUpdateCookie({
@@ -121,10 +111,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
});
} catch (error: any) {
if ("failedAttempts" in error && error.failedAttempts) {
- const lockoutSettings = await getLockoutSettings({
- serviceUrl,
- orgId: command.organization,
- });
+ const lockoutSettings = await getLockoutSettings({ serviceConfig, orgId: command.organization });
const hasLimit =
lockoutSettings?.maxPasswordAttempts !== undefined && lockoutSettings?.maxPasswordAttempts > BigInt(0);
@@ -149,10 +136,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
// this is a fake error message to hide that the user does not even exist
return { error: t("errors.couldNotVerifyPassword") };
} else {
- loginSettings = await getLoginSettings({
- serviceUrl,
- organization: sessionCookie.organization,
- });
+ loginSettings = await getLoginSettings({ serviceConfig, organization: sessionCookie.organization });
if (!loginSettings) {
return { error: "Could not load login settings" };
@@ -177,10 +161,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
});
} catch (error: any) {
if ("failedAttempts" in error && error.failedAttempts) {
- const lockoutSettings = await getLockoutSettings({
- serviceUrl,
- orgId: command.organization,
- });
+ const lockoutSettings = await getLockoutSettings({ serviceConfig, orgId: command.organization });
const hasLimit =
lockoutSettings?.maxPasswordAttempts !== undefined && lockoutSettings?.maxPasswordAttempts > BigInt(0);
@@ -202,10 +183,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
return { error: t("errors.couldNotCreateSessionForUser") };
}
- const userResponse = await getUserByID({
- serviceUrl,
- userId: session?.factors?.user?.id,
- });
+ const userResponse = await getUserByID({ serviceConfig, userId: session?.factors?.user?.id });
if (!userResponse.user) {
return { error: t("errors.userNotFound") };
@@ -216,7 +194,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
if (!loginSettings) {
loginSettings = await getLoginSettings({
- serviceUrl,
+ serviceConfig,
organization: command.organization ?? session.factors?.user?.organizationId,
});
}
@@ -228,7 +206,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
const humanUser = user.type.case === "human" ? user.type.value : undefined;
const expirySettings = await getPasswordExpirySettings({
- serviceUrl,
+ serviceConfig,
orgId: command.organization ?? session.factors?.user?.organizationId,
});
@@ -260,10 +238,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
// if password, check if user has MFA methods
let authMethods;
if (command.checks && command.checks.password && session.factors?.user?.id) {
- const response = await listAuthenticationMethodTypes({
- serviceUrl,
- userId: session.factors.user.id,
- });
+ const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
if (response.authMethodTypes && response.authMethodTypes.length) {
authMethods = response.authMethodTypes;
}
@@ -274,7 +249,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
}
const mfaFactorCheck = await checkMFAFactors(
- serviceUrl,
+ serviceConfig,
session,
loginSettings,
authMethods,
@@ -331,14 +306,11 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
// this function lets users with code set a password or users with valid User Verification Check
export async function changePassword(command: { code?: string; userId: string; password: string }) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("password");
// check for init state
- const { user } = await getUserByID({
- serviceUrl,
- userId: command.userId,
- });
+ const { user } = await getUserByID({ serviceConfig, userId: command.userId });
if (!user || user.userId !== command.userId) {
return { error: t("errors.couldNotSendResetLink") };
@@ -351,10 +323,7 @@ export async function changePassword(command: { code?: string; userId: string; p
// check if the user has no password set in order to set a password
if (!command.code) {
- const authmethods = await listAuthenticationMethodTypes({
- serviceUrl,
- userId,
- });
+ const authmethods = await listAuthenticationMethodTypes({ serviceConfig, userId });
// if the user has no authmethods set, we need to check if the user was verified
if (authmethods.authMethodTypes.length !== 0) {
@@ -371,12 +340,7 @@ export async function changePassword(command: { code?: string; userId: string; p
}
}
- return setUserPassword({
- serviceUrl,
- userId,
- password: command.password,
- code: command.code,
- });
+ return setUserPassword({ serviceConfig, userId, password: command.password, code: command.code });
}
type CheckSessionAndSetPasswordCommand = {
@@ -386,7 +350,7 @@ type CheckSessionAndSetPasswordCommand = {
export async function checkSessionAndSetPassword({ sessionId, password }: CheckSessionAndSetPasswordCommand) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("password");
let sessionCookie;
@@ -400,7 +364,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
let session;
try {
const sessionResponse = await getSession({
- serviceUrl,
+ serviceConfig,
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
@@ -424,10 +388,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
// check if the user has no password set in order to set a password
let authmethods;
try {
- authmethods = await listAuthenticationMethodTypes({
- serviceUrl,
- userId: session.factors.user.id,
- });
+ authmethods = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
} catch (error) {
console.error("Error getting auth methods:", error);
return { error: "Could not load auth methods" };
@@ -439,10 +400,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
let loginSettings;
try {
- loginSettings = await getLoginSettings({
- serviceUrl,
- organization: session.factors.user.organizationId,
- });
+ loginSettings = await getLoginSettings({ serviceConfig, organization: session.factors.user.organizationId });
} catch (error) {
console.error("Error getting login settings:", error);
return { error: "Could not load login settings" };
@@ -453,7 +411,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
// if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user
if (forceMfa) {
console.log("Set password using service account due to enforced MFA without existing MFA methods");
- return setPassword({ serviceUrl, payload }).catch((error) => {
+ return setPassword({ serviceConfig, payload }).catch((error) => {
// throw error if failed precondition (ex. User is not yet initialized)
if (error.code === 9 && error.message) {
return { error: t("errors.failedPrecondition") };
@@ -461,16 +419,16 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
return { error: "Could not set password" };
});
} else {
- const transport = async (serviceUrl: string, token: string) => {
- return createServerTransport(token, serviceUrl);
+ const transport = async (serviceConfig: ServiceConfig, token: string) => {
+ return createServerTransport(token, serviceConfig);
};
- const myUserService = async (serviceUrl: string, sessionToken: string) => {
- const transportPromise = await transport(serviceUrl, sessionToken);
+ const myUserService = async (serviceConfig: ServiceConfig, sessionToken: string) => {
+ const transportPromise = await transport(serviceConfig, sessionToken);
return createUserServiceClient(transportPromise);
};
- const selfService = await myUserService(serviceUrl, `${sessionCookie.token}`);
+ const selfService = await myUserService(serviceConfig, sessionCookie.token);
return selfService
.setPassword(
diff --git a/apps/login/src/lib/server/register.ts b/apps/login/src/lib/server/register.ts
index fb6656f9d..daa73de37 100644
--- a/apps/login/src/lib/server/register.ts
+++ b/apps/login/src/lib/server/register.ts
@@ -7,7 +7,7 @@ import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { ChecksJson, ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { cookies, headers } from "next/headers";
import { getTranslations } from "next-intl/server";
-import { getServiceUrlFromHeaders } from "../service-url";
+import { getServiceConfig } from "../service-url";
import { checkEmailVerification, checkMFAFactors } from "../verify-helper";
import { getOrSetFingerprintId } from "../fingerprint";
import crypto from "crypto";
@@ -30,11 +30,9 @@ export type RegisterUserResponse = {
export async function registerUser(command: RegisterUserCommand) {
const t = await getTranslations("register");
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
- const addResponse = await addHumanUser({
- serviceUrl,
- email: command.email,
+ const addResponse = await addHumanUser({ serviceConfig, email: command.email,
firstName: command.firstName,
lastName: command.lastName,
password: command.password ? command.password : undefined,
@@ -45,9 +43,7 @@ export async function registerUser(command: RegisterUserCommand) {
return { error: t("errors.couldNotCreateUser") };
}
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization: command.organization,
+ const loginSettings = await getLoginSettings({ serviceConfig, organization: command.organization,
});
let checkPayload: any = {
@@ -100,9 +96,7 @@ export async function registerUser(command: RegisterUserCommand) {
return { redirect: "/passkey/set?" + params };
} else {
- const userResponse = await getUserByID({
- serviceUrl,
- userId: session?.factors?.user?.id,
+ const userResponse = await getUserByID({ serviceConfig, userId: session?.factors?.user?.id,
});
if (!userResponse.user) {
@@ -162,11 +156,9 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
const t = await getTranslations("register");
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
- const addUserResponse = await addHumanUser({
- serviceUrl,
- email: command.email,
+ const addUserResponse = await addHumanUser({ serviceConfig, email: command.email,
firstName: command.firstName,
lastName: command.lastName,
organization: command.organization,
@@ -176,14 +168,10 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
return { error: t("errors.couldNotCreateUser") };
}
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization: command.organization,
+ const loginSettings = await getLoginSettings({ serviceConfig, organization: command.organization,
});
- const idpLink = await addIDPLink({
- serviceUrl,
- idp: {
+ const idpLink = await addIDPLink({ serviceConfig, idp: {
id: command.idpId,
userId: command.idpUserId,
userName: command.idpUserName,
@@ -207,7 +195,7 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
}
// const userResponse = await getUserByID({
- // serviceUrl,
+ // serviceConfig.baseUrl,
// userId: session?.factors?.user?.id,
// });
@@ -227,9 +215,7 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
// check if user has MFA methods
let authMethods;
if (session.factors?.user?.id) {
- const response = await listAuthenticationMethodTypes({
- serviceUrl,
- userId: session.factors.user.id,
+ const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id,
});
if (response.authMethodTypes && response.authMethodTypes.length) {
authMethods = response.authMethodTypes;
@@ -239,7 +225,7 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
// Always check MFA factors, even if no auth methods are configured
// This ensures that force MFA settings are respected
const mfaFactorCheck = await checkMFAFactors(
- serviceUrl,
+ serviceConfig,
session,
loginSettings,
authMethods || [], // Pass empty array if no auth methods
diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts
index 43839cf78..dd57f6577 100644
--- a/apps/login/src/lib/server/session.ts
+++ b/apps/login/src/lib/server/session.ts
@@ -21,8 +21,8 @@ import {
getSessionCookieByLoginName,
removeSessionFromCookie,
} from "../cookies";
-import { getServiceUrlFromHeaders } from "../service-url";
-import { getOriginalHost } from "./host";
+import { getServiceConfig } from "../service-url";
+import { getPublicHost } from "./host";
export async function skipMFAAndContinueWithNextUrl({
userId,
@@ -38,14 +38,11 @@ export async function skipMFAAndContinueWithNextUrl({
organization?: string;
}): Promise<{ redirect: string } | { error: string }> {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization: organization,
- });
+ const loginSettings = await getLoginSettings({ serviceConfig, organization: organization });
- await humanMFAInitSkipped({ serviceUrl, userId });
+ await humanMFAInitSkipped({ serviceConfig, userId });
if (requestId && sessionId) {
return completeFlowOrGetUrl(
@@ -73,14 +70,11 @@ export type ContinueWithSessionCommand = Session & { requestId?: string };
export async function continueWithSession({ requestId, ...session }: ContinueWithSessionCommand) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("error");
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization: session.factors?.user?.organizationId,
- });
+ const loginSettings = await getLoginSettings({ serviceConfig, organization: session.factors?.user?.organizationId });
if (requestId && session.id && session.factors?.user) {
return completeFlowOrGetUrl(
@@ -130,8 +124,8 @@ export async function updateSession(options: UpdateSessionCommand) {
}
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
- const host = await getOriginalHost();
+ const { serviceConfig } = getServiceConfig(_headers);
+ const host = getPublicHost(_headers);
if (!host) {
return { error: "Could not get host" };
@@ -143,10 +137,7 @@ export async function updateSession(options: UpdateSessionCommand) {
challenges.webAuthN.domain = hostname;
}
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization,
- });
+ const loginSettings = await getLoginSettings({ serviceConfig, organization });
let lifetime = checks?.webAuthN
? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey
@@ -177,10 +168,7 @@ export async function updateSession(options: UpdateSessionCommand) {
// if password, check if user has MFA methods
let authMethods;
if (checks && checks.password && session.factors?.user?.id) {
- const response = await listAuthenticationMethodTypes({
- serviceUrl,
- userId: session.factors.user.id,
- });
+ const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
if (response.authMethodTypes && response.authMethodTypes.length) {
authMethods = response.authMethodTypes;
}
@@ -200,19 +188,19 @@ type ClearSessionOptions = {
export async function clearSession(options: ClearSessionOptions) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const { sessionId } = options;
const sessionCookie = await getSessionCookieById({ sessionId });
const deleteResponse = await deleteSession({
- serviceUrl,
+ serviceConfig,
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
- const securitySettings = await getSecuritySettings({ serviceUrl });
+ const securitySettings = await getSecuritySettings({ serviceConfig });
const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
if (!deleteResponse) {
diff --git a/apps/login/src/lib/server/u2f.ts b/apps/login/src/lib/server/u2f.ts
index 69d0454e9..c73ee4eac 100644
--- a/apps/login/src/lib/server/u2f.ts
+++ b/apps/login/src/lib/server/u2f.ts
@@ -6,8 +6,8 @@ import { VerifyU2FRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/
import { headers } from "next/headers";
import { userAgent } from "next/server";
import { getSessionCookieById } from "../cookies";
-import { getServiceUrlFromHeaders } from "../service-url";
-import { getOriginalHost } from "./host";
+import { getServiceConfig } from "../service-url";
+import { getPublicHost } from "./host";
type RegisterU2FCommand = {
sessionId: string;
@@ -22,8 +22,8 @@ type VerifyU2FCommand = {
export async function addU2F(command: RegisterU2FCommand) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
- const host = await getOriginalHost();
+ const { serviceConfig } = getServiceConfig(_headers);
+ const host = getPublicHost(_headers);
const sessionCookie = await getSessionCookieById({
sessionId: command.sessionId,
@@ -33,11 +33,7 @@ export async function addU2F(command: RegisterU2FCommand) {
return { error: "Could not get session" };
}
- const session = await getSession({
- serviceUrl,
- sessionId: sessionCookie.id,
- sessionToken: sessionCookie.token,
- });
+ const session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
const [hostname] = host.split(":");
@@ -51,12 +47,12 @@ export async function addU2F(command: RegisterU2FCommand) {
return { error: "Could not get session" };
}
- return registerU2F({ serviceUrl, userId, domain: hostname });
+ return registerU2F({ serviceConfig, userId, domain: hostname });
}
export async function verifyU2F(command: VerifyU2FCommand) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
let passkeyName = command.passkeyName;
if (!passkeyName) {
const headersList = await headers();
@@ -71,11 +67,7 @@ export async function verifyU2F(command: VerifyU2FCommand) {
sessionId: command.sessionId,
});
- const session = await getSession({
- serviceUrl,
- sessionId: sessionCookie.id,
- sessionToken: sessionCookie.token,
- });
+ const session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
const userId = session?.session?.factors?.user?.id;
@@ -90,5 +82,5 @@ export async function verifyU2F(command: VerifyU2FCommand) {
userId,
});
- return verifyU2FRegistration({ serviceUrl, request });
+ return verifyU2FRegistration({ serviceConfig, request });
}
diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts
index 3fb450666..c5ad14103 100644
--- a/apps/login/src/lib/server/verify.ts
+++ b/apps/login/src/lib/server/verify.ts
@@ -20,30 +20,26 @@ import { cookies, headers } from "next/headers";
import { completeFlowOrGetUrl } from "../client";
import { getSessionCookieByLoginName } from "../cookies";
import { getOrSetFingerprintId } from "../fingerprint";
-import { getServiceUrlFromHeaders } from "../service-url";
+import { getServiceConfig } from "../service-url";
import { loadMostRecentSession } from "../session";
import { checkMFAFactors } from "../verify-helper";
import { createSessionAndUpdateCookie } from "./cookie";
-import { getOriginalHostWithProtocol } from "./host";
+import { getPublicHostWithProtocol } from "./host";
import { getTranslations } from "next-intl/server";
export async function verifyTOTP(code: string, loginName?: string, organization?: string) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
return loadMostRecentSession({
- serviceUrl,
+ serviceConfig,
sessionParams: {
loginName,
organization,
},
}).then((session) => {
if (session?.factors?.user?.id) {
- return verifyTOTPRegistration({
- serviceUrl,
- code,
- userId: session.factors.user.id,
- });
+ return verifyTOTPRegistration({ serviceConfig, code, userId: session.factors.user.id });
} else {
throw Error("No user id found in session.");
}
@@ -62,22 +58,14 @@ type VerifyUserByEmailCommand = {
export async function sendVerification(command: VerifyUserByEmailCommand) {
const t = await getTranslations("verify");
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
const verifyResponse = command.isInvite
- ? await verifyInviteCode({
- serviceUrl,
- userId: command.userId,
- verificationCode: command.code,
- }).catch((error) => {
+ ? await verifyInviteCode({ serviceConfig, userId: command.userId, verificationCode: command.code }).catch((error) => {
console.warn(error);
return { error: t("errors.couldNotVerifyInvite") };
})
- : await verifyEmail({
- serviceUrl,
- userId: command.userId,
- verificationCode: command.code,
- }).catch((error) => {
+ : await verifyEmail({ serviceConfig, userId: command.userId, verificationCode: command.code }).catch((error) => {
console.warn(error);
return { error: t("errors.couldNotVerifyEmail") };
});
@@ -91,10 +79,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
}
let session: Session | undefined;
- const userResponse = await getUserByID({
- serviceUrl,
- userId: command.userId,
- });
+ const userResponse = await getUserByID({ serviceConfig, userId: command.userId });
if (!userResponse || !userResponse.user) {
return { error: t("errors.couldNotLoadUser") };
@@ -110,22 +95,17 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
});
if (sessionCookie) {
- session = await getSession({
- serviceUrl,
- sessionId: sessionCookie.id,
- sessionToken: sessionCookie.token,
- }).then((response) => {
- if (response?.session) {
- return response.session;
- }
- });
+ session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token }).then(
+ (response) => {
+ if (response?.session) {
+ return response.session;
+ }
+ },
+ );
}
// load auth methods for user
- const authMethodResponse = await listAuthenticationMethodTypes({
- serviceUrl,
- userId: user.userId,
- });
+ const authMethodResponse = await listAuthenticationMethodTypes({ serviceConfig, userId: user.userId });
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
return { error: t("errors.couldNotLoadAuthenticators") };
@@ -203,14 +183,11 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
return { redirect: `/verify/success?${verifySuccessParams}` };
}
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization: user.details?.resourceOwner,
- });
+ const loginSettings = await getLoginSettings({ serviceConfig, organization: user.details?.resourceOwner });
// redirect to mfa factor if user has one, or redirect to set one up
const mfaFactorCheck = await checkMFAFactors(
- serviceUrl,
+ serviceConfig,
session,
loginSettings,
authMethodResponse.authMethodTypes,
@@ -253,14 +230,14 @@ type resendVerifyEmailCommand = {
export async function resendVerification(command: resendVerifyEmailCommand) {
const t = await getTranslations("verify");
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
- const hostWithProtocol = await getOriginalHostWithProtocol();
+ const { serviceConfig } = getServiceConfig(_headers);
+ const hostWithProtocol = await getPublicHostWithProtocol(_headers);
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
return command.isInvite
? createInviteCode({
- serviceUrl,
+ serviceConfig,
userId: command.userId,
urlTemplate:
`${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
@@ -272,8 +249,8 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
return { error: t("errors.couldNotResendInvite") };
})
: zitadelSendEmailCode({
+ serviceConfig,
userId: command.userId,
- serviceUrl,
urlTemplate:
`${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
(command.requestId ? `&requestId=${command.requestId}` : ""),
@@ -287,22 +264,14 @@ type SendEmailCommand = {
export async function sendEmailCode(command: SendEmailCommand) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
- return zitadelSendEmailCode({
- serviceUrl,
- userId: command.userId,
- urlTemplate: command.urlTemplate,
- });
+ return zitadelSendEmailCode({ serviceConfig, userId: command.userId, urlTemplate: command.urlTemplate });
}
export async function sendInviteEmailCode(command: SendEmailCommand) {
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
- return createInviteCode({
- serviceUrl,
- userId: command.userId,
- urlTemplate: command.urlTemplate,
- });
+ return createInviteCode({ serviceConfig, userId: command.userId, urlTemplate: command.urlTemplate });
}
diff --git a/apps/login/src/lib/service-url.test.ts b/apps/login/src/lib/service-url.test.ts
new file mode 100644
index 000000000..be8408af9
--- /dev/null
+++ b/apps/login/src/lib/service-url.test.ts
@@ -0,0 +1,217 @@
+import { describe, expect, test, beforeEach, afterEach, vi } from "vitest";
+import { getServiceConfig, constructUrl } from "./service-url";
+import { NextRequest } from "next/server";
+
+describe("Service URL utilities", () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ process.env = { ...originalEnv };
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ });
+
+ describe("getServiceConfig", () => {
+ test("should throw when ZITADEL_API_URL is not set", () => {
+ process.env.ZITADEL_API_URL = undefined as any;
+
+ const mockHeaders = {
+ get: vi.fn(() => null),
+ } as any;
+
+ expect(() => getServiceConfig(mockHeaders)).toThrow("ZITADEL_API_URL is not set");
+ });
+
+ test("should return only baseUrl when x-zitadel-forward-host is not present (self-hosted)", () => {
+ process.env.ZITADEL_API_URL = "https://zitadel.mycompany.com";
+
+ const mockHeaders = {
+ get: vi.fn((key: string) => {
+ if (key === "x-zitadel-forward-host") return null;
+ if (key === "host") return "mycompany.com";
+ return null;
+ }),
+ } as any;
+
+ const result = getServiceConfig(mockHeaders);
+
+ expect(result.serviceConfig.baseUrl).toBe("https://zitadel.mycompany.com");
+ expect(result.serviceConfig.instanceHost).toBeUndefined();
+ expect(result.serviceConfig.publicHost).toBe("mycompany.com");
+ });
+
+ test("should use x-zitadel-forward-host when present (multi-tenant)", () => {
+ process.env.ZITADEL_API_URL = "https://api.zitadel.cloud";
+
+ const mockHeaders = {
+ get: vi.fn((key: string) => {
+ if (key === "x-zitadel-forward-host") return "customer.zitadel.cloud";
+ if (key === "host") return "customer.zitadel.cloud";
+ return null;
+ }),
+ } as any;
+
+ const result = getServiceConfig(mockHeaders);
+
+ expect(result.serviceConfig.baseUrl).toBe("https://api.zitadel.cloud");
+ expect(result.serviceConfig.instanceHost).toBe("customer.zitadel.cloud");
+ expect(result.serviceConfig.publicHost).toBe("customer.zitadel.cloud");
+ });
+
+ test("should strip protocol from instanceHost and publicHost", () => {
+ process.env.ZITADEL_API_URL = "https://api.zitadel.cloud";
+
+ const mockHeaders = {
+ get: vi.fn((key: string) => {
+ if (key === "x-zitadel-forward-host") return "https://customer.zitadel.cloud";
+ if (key === "host") return "customer.zitadel.cloud";
+ return null;
+ }),
+ } as any;
+
+ const result = getServiceConfig(mockHeaders);
+
+ expect(result.serviceConfig.instanceHost).toBe("customer.zitadel.cloud");
+ expect(result.serviceConfig.publicHost).toBe("customer.zitadel.cloud");
+ });
+
+ test("should throw when host header is missing", () => {
+ process.env.ZITADEL_API_URL = "https://api.zitadel.cloud";
+
+ const mockHeaders = {
+ get: vi.fn((key: string) => {
+ if (key === "x-zitadel-forward-host") return null;
+ if (key === "host") return null;
+ return null;
+ }),
+ } as any;
+
+ expect(() => getServiceConfig(mockHeaders)).toThrow("No host found in headers");
+ });
+
+ test("should handle host with port number", () => {
+ process.env.ZITADEL_API_URL = "https://api.zitadel.cloud";
+
+ const mockHeaders = {
+ get: vi.fn((key: string) => {
+ if (key === "x-zitadel-forward-host") return "customer.zitadel.cloud:443";
+ if (key === "host") return "customer.zitadel.cloud:443";
+ return null;
+ }),
+ } as any;
+
+ const result = getServiceConfig(mockHeaders);
+
+ expect(result.serviceConfig.publicHost).toBe("customer.zitadel.cloud:443");
+ });
+ });
+
+ describe("constructUrl", () => {
+ test("should construct URL with x-zitadel-forward-host when present", () => {
+ process.env.NEXT_PUBLIC_BASE_PATH = "";
+ const mockRequest = {
+ headers: {
+ get: vi.fn((key: string) => {
+ if (key === "x-zitadel-forward-host") return "customer.zitadel.cloud";
+ if (key === "host") return "customer.zitadel.cloud";
+ return null;
+ }),
+ },
+ nextUrl: {
+ protocol: "https:",
+ },
+ } as any;
+
+ const result = constructUrl(mockRequest as NextRequest, "/test");
+
+ expect(result.hostname).toBe("customer.zitadel.cloud");
+ expect(result.pathname).toBe("/test");
+ expect(result.protocol).toBe("https:");
+ });
+
+ test("should fall back to x-forwarded-host when x-zitadel-forward-host is not present", () => {
+ process.env.NEXT_PUBLIC_BASE_PATH = "";
+ const mockRequest = {
+ headers: {
+ get: vi.fn((key: string) => {
+ if (key === "x-zitadel-forward-host") return null;
+ if (key === "x-forwarded-host") return "mycompany.com";
+ return null;
+ }),
+ },
+ nextUrl: {
+ protocol: "https:",
+ },
+ } as any;
+
+ const result = constructUrl(mockRequest as NextRequest, "/oauth/authorize");
+
+ expect(result.hostname).toBe("mycompany.com");
+ expect(result.pathname).toBe("/oauth/authorize");
+ });
+
+ test("should fall back to host header when no forwarded headers present", () => {
+ const mockRequest = {
+ headers: {
+ get: vi.fn((key: string) => {
+ if (key === "x-zitadel-forward-host") return null;
+ if (key === "x-forwarded-host") return null;
+ if (key === "host") return "localhost:3000";
+ return null;
+ }),
+ },
+ nextUrl: {
+ protocol: "http:",
+ },
+ } as any;
+
+ const result = constructUrl(mockRequest as NextRequest, "/test");
+
+ expect(result.hostname).toBe("localhost");
+ expect(result.port).toBe("3000");
+ });
+
+ test("should use protocol from nextUrl.protocol (not from headers)", () => {
+ const mockRequest = {
+ headers: {
+ get: vi.fn((key: string) => {
+ // Even if x-forwarded-proto is present, it should be ignored
+ if (key === "x-forwarded-proto") return "http";
+ if (key === "host") return "example.com";
+ return null;
+ }),
+ },
+ nextUrl: {
+ protocol: "https:", // This should be used
+ },
+ } as any;
+
+ const result = constructUrl(mockRequest as NextRequest, "/test");
+
+ // Should use https: from nextUrl.protocol, not http from header
+ expect(result.protocol).toBe("https:");
+ });
+
+ test("should include base path when NEXT_PUBLIC_BASE_PATH is set", () => {
+ process.env.NEXT_PUBLIC_BASE_PATH = "/login";
+
+ const mockRequest = {
+ headers: {
+ get: vi.fn((key: string) => {
+ if (key === "host") return "example.com";
+ return null;
+ }),
+ },
+ nextUrl: {
+ protocol: "https:",
+ },
+ } as any;
+
+ const result = constructUrl(mockRequest as NextRequest, "/test");
+
+ expect(result.pathname).toBe("/login/test");
+ });
+ });
+});
diff --git a/apps/login/src/lib/service-url.ts b/apps/login/src/lib/service-url.ts
index cc5e3f04c..02a2160de 100644
--- a/apps/login/src/lib/service-url.ts
+++ b/apps/login/src/lib/service-url.ts
@@ -1,58 +1,47 @@
import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
import { NextRequest } from "next/server";
+import { ServiceConfig } from "./zitadel";
+import { getInstanceHost, getPublicHost } from "./server/host";
/**
- * Extracts the service url and region from the headers if used in a multitenant context (host, x-zitadel-forward-host header)
- * or falls back to the ZITADEL_API_URL for a self hosting deployment
- * or falls back to the host header for a self hosting deployment using custom domains
- * @param headers
- * @returns the service url and region from the headers
- * @throws if the service url could not be determined
+ * Extracts the service URL based on deployment mode and configuration.
*
+ * Priority:
+ * 1. ZITADEL_API_URL (required) - Used by both self-hosted and multi-tenant
+ * 2. x-zitadel-forward-host (multi-tenant only) - Set by Zitadel proxy
+ * 3. host header (multi-tenant fallback) - For dynamic host resolution
+ *
+ * @param headers - Request headers
+ * @returns Object containing the service Configuration
+ * @throws Error if the service Configuration could not be determined
*/
-export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): {
- serviceUrl: string;
-} {
- let instanceUrl;
- const forwardedHost = headers.get("x-zitadel-forward-host");
- // use the forwarded host if available (multitenant), otherwise fall back to the host of the deployment itself
- if (forwardedHost) {
- instanceUrl = forwardedHost;
- instanceUrl = instanceUrl.startsWith("http://")
- ? instanceUrl
- : `https://${instanceUrl}`;
- } else if (process.env.ZITADEL_API_URL) {
- instanceUrl = process.env.ZITADEL_API_URL;
- } else {
- const host = headers.get("host");
+function stripProtocol(url: string): string {
+ return url.replace(/^https?:\/\//, "");
+}
- if (host) {
- const [hostname] = host.split(":");
- if (hostname !== "localhost") {
- instanceUrl = host.startsWith("http") ? host : `https://${host}`;
- }
- }
+export function getServiceConfig(headers: ReadonlyHeaders): { serviceConfig: ServiceConfig } {
+ if (!process.env.ZITADEL_API_URL) {
+ throw new Error("ZITADEL_API_URL is not set");
}
- if (!instanceUrl) {
- throw new Error("Service URL could not be determined");
- }
+ // use forwarded host from proxy - headers are forwarded to the APIs.
+ const instanceHost = getInstanceHost(headers);
+ const publicHost = getPublicHost(headers);
return {
- serviceUrl: instanceUrl,
+ serviceConfig: {
+ baseUrl: process.env.ZITADEL_API_URL,
+ ...(instanceHost && { instanceHost: stripProtocol(instanceHost) }),
+ ...(publicHost && { publicHost: stripProtocol(publicHost) }),
+ },
};
}
export function constructUrl(request: NextRequest, path: string) {
- const forwardedProto = request.headers.get("x-forwarded-proto")
- ? `${request.headers.get("x-forwarded-proto")}:`
- : request.nextUrl.protocol;
+ const protocol = request.nextUrl.protocol;
- const forwardedHost =
- request.headers.get("x-zitadel-forward-host") ??
- request.headers.get("x-forwarded-host") ??
- request.headers.get("host");
+ const forwardedHost = getPublicHost(request.headers);
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
- return new URL(`${basePath}${path}`, `${forwardedProto}//${forwardedHost}`);
+ return new URL(`${basePath}${path}`, `${protocol}//${forwardedHost}`);
}
diff --git a/apps/login/src/lib/service.ts b/apps/login/src/lib/service.ts
index f7e81cc9d..29e56d016 100644
--- a/apps/login/src/lib/service.ts
+++ b/apps/login/src/lib/service.ts
@@ -7,7 +7,8 @@ import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_servic
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { systemAPIToken } from "./api";
-import { createServerTransport } from "./zitadel";
+import { hasSystemUserCredentials, hasServiceUserToken } from "./deployment";
+import { createServerTransport, ServiceConfig } from "./zitadel";
type ServiceClass =
| typeof IdentityProviderService
@@ -18,32 +19,27 @@ type ServiceClass =
| typeof SettingsService
| typeof SAMLService;
-export async function createServiceForHost(
- service: T,
- serviceUrl: string,
-) {
+export async function createServiceForHost(service: T, serviceConfig: ServiceConfig) {
let token;
- // if we are running in a multitenancy context, use the system user token
- if (
- process.env.AUDIENCE &&
- process.env.SYSTEM_USER_ID &&
- process.env.SYSTEM_USER_PRIVATE_KEY
- ) {
+ // Determine authentication method based on available credentials
+ // Prefer system user JWT if available, fallback to service user token
+ if (hasSystemUserCredentials()) {
token = await systemAPIToken();
- } else if (process.env.ZITADEL_SERVICE_USER_TOKEN) {
+ } else if (hasServiceUserToken()) {
+ // Use service user token authentication (self-hosted)
token = process.env.ZITADEL_SERVICE_USER_TOKEN;
+ } else {
+ throw new Error(
+ "No authentication credentials found. Set either system user credentials (AUDIENCE, SYSTEM_USER_ID, SYSTEM_USER_PRIVATE_KEY) or ZITADEL_SERVICE_USER_TOKEN",
+ );
}
- if (!serviceUrl) {
- throw new Error("No instance url found");
+ if (!serviceConfig) {
+ throw new Error("No service config found");
}
- if (!token) {
- throw new Error("No token found");
- }
-
- const transport = createServerTransport(token, serviceUrl);
+ const transport = createServerTransport(token, serviceConfig);
return createClientFor(service)(transport);
}
diff --git a/apps/login/src/lib/session.test.ts b/apps/login/src/lib/session.test.ts
index a9d75ee08..1a7c0519e 100644
--- a/apps/login/src/lib/session.test.ts
+++ b/apps/login/src/lib/session.test.ts
@@ -101,7 +101,7 @@ describe("isSessionValid", () => {
factors: undefined,
});
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("Session has no user");
@@ -120,7 +120,7 @@ describe("isSessionValid", () => {
authMethodTypes: [],
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("Session is expired", expect.any(String));
@@ -147,7 +147,7 @@ describe("isSessionValid", () => {
authMethodTypes: [],
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
});
@@ -179,7 +179,7 @@ describe("isSessionValid", () => {
forceMfaLocalOnly: false,
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
@@ -207,7 +207,7 @@ describe("isSessionValid", () => {
forceMfaLocalOnly: false,
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
@@ -242,7 +242,7 @@ describe("isSessionValid", () => {
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith(
@@ -278,7 +278,7 @@ describe("isSessionValid", () => {
authMethodTypes: [AuthenticationMethodType.OTP_EMAIL],
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
@@ -307,7 +307,7 @@ describe("isSessionValid", () => {
authMethodTypes: [AuthenticationMethodType.U2F],
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
@@ -342,7 +342,7 @@ describe("isSessionValid", () => {
authMethodTypes: [AuthenticationMethodType.TOTP, AuthenticationMethodType.OTP_EMAIL],
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
@@ -372,7 +372,7 @@ describe("isSessionValid", () => {
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
@@ -409,7 +409,7 @@ describe("isSessionValid", () => {
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
@@ -447,7 +447,7 @@ describe("isSessionValid", () => {
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith(
@@ -499,7 +499,7 @@ describe("isSessionValid", () => {
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
// This should be true - if it's false, the original bug still exists
expect(result).toBe(true);
@@ -539,7 +539,7 @@ describe("isSessionValid", () => {
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
// With our fix, this should be true (session is valid)
// With the old logic, this would have been false (bug)
@@ -576,7 +576,7 @@ describe("isSessionValid", () => {
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
@@ -611,7 +611,7 @@ describe("isSessionValid", () => {
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith("Session has no valid multifactor", expect.any(Object));
@@ -647,7 +647,7 @@ describe("isSessionValid", () => {
forceMfaLocalOnly: false,
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
@@ -681,7 +681,7 @@ describe("isSessionValid", () => {
forceMfaLocalOnly: true,
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
@@ -716,15 +716,15 @@ describe("isSessionValid", () => {
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
- serviceUrl: mockServiceUrl,
+ serviceConfig: { baseUrl: mockServiceUrl },
organization: mockOrganizationId,
});
expect(zitadelModule.listAuthenticationMethodTypes).toHaveBeenCalledWith({
- serviceUrl: mockServiceUrl,
+ serviceConfig: { baseUrl: mockServiceUrl },
userId: mockUserId,
});
consoleSpy.mockRestore();
@@ -777,7 +777,7 @@ describe("isSessionValid", () => {
},
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith(
@@ -831,7 +831,7 @@ describe("isSessionValid", () => {
},
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
@@ -866,7 +866,7 @@ describe("isSessionValid", () => {
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
// getUserByID should not be called when EMAIL_VERIFICATION is disabled
@@ -902,7 +902,7 @@ describe("isSessionValid", () => {
forceMfaLocalOnly: false,
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
@@ -938,12 +938,12 @@ describe("isSessionValid", () => {
authMethodTypes: [],
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
- serviceUrl: mockServiceUrl,
+ serviceConfig: { baseUrl: mockServiceUrl },
organization: mockOrganizationId,
});
});
@@ -979,16 +979,16 @@ describe("isSessionValid", () => {
authMethodTypes: [AuthenticationMethodType.TOTP, AuthenticationMethodType.OTP_EMAIL],
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(false);
expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
- serviceUrl: mockServiceUrl,
+ serviceConfig: { baseUrl: mockServiceUrl },
organization: mockOrganizationId,
});
expect(zitadelModule.listAuthenticationMethodTypes).toHaveBeenCalledWith({
- serviceUrl: mockServiceUrl,
+ serviceConfig: { baseUrl: mockServiceUrl },
userId: mockUserId,
});
});
@@ -1024,12 +1024,12 @@ describe("isSessionValid", () => {
authMethodTypes: [AuthenticationMethodType.TOTP, AuthenticationMethodType.OTP_EMAIL],
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
- serviceUrl: mockServiceUrl,
+ serviceConfig: { baseUrl: mockServiceUrl },
organization: mockOrganizationId,
});
// Should not call listAuthenticationMethodTypes since shouldEnforceMFA returned false
@@ -1067,7 +1067,7 @@ describe("isSessionValid", () => {
authMethodTypes: [AuthenticationMethodType.TOTP],
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
@@ -1105,12 +1105,12 @@ describe("isSessionValid", () => {
authMethodTypes: [AuthenticationMethodType.TOTP],
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
- serviceUrl: mockServiceUrl,
+ serviceConfig: { baseUrl: mockServiceUrl },
organization: mockOrganizationId,
});
// Should not call listAuthenticationMethodTypes since shouldEnforceMFA returned false
@@ -1146,7 +1146,7 @@ describe("isSessionValid", () => {
forceMfaLocalOnly: false,
} as any);
- const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
+ const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
expect(result).toBe(true);
});
diff --git a/apps/login/src/lib/session.ts b/apps/login/src/lib/session.ts
index 83bca3d15..53c49c8db 100644
--- a/apps/login/src/lib/session.ts
+++ b/apps/login/src/lib/session.ts
@@ -6,37 +6,30 @@ import { GetSessionResponse } from "@zitadel/proto/zitadel/session/v2/session_se
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getMostRecentCookieWithLoginname } from "./cookies";
import { shouldEnforceMFA } from "./verify-helper";
-import { getLoginSettings, getSession, getUserByID, listAuthenticationMethodTypes } from "./zitadel";
+import { getLoginSettings, getSession, getUserByID, listAuthenticationMethodTypes, ServiceConfig } from "./zitadel";
type LoadMostRecentSessionParams = {
- serviceUrl: string;
+ serviceConfig: ServiceConfig;
sessionParams: {
loginName?: string;
organization?: string;
};
};
-export async function loadMostRecentSession({
- serviceUrl,
- sessionParams,
-}: LoadMostRecentSessionParams): Promise {
+export async function loadMostRecentSession({ serviceConfig, sessionParams }: LoadMostRecentSessionParams): Promise {
const recent = await getMostRecentCookieWithLoginname({
loginName: sessionParams.loginName,
organization: sessionParams.organization,
});
- return getSession({
- serviceUrl,
- sessionId: recent.id,
- sessionToken: recent.token,
- }).then((resp: GetSessionResponse) => resp.session);
+ return getSession({ serviceConfig, sessionId: recent.id, sessionToken: recent.token }).then((resp: GetSessionResponse) => resp.session);
}
/**
* mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.)
* to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId);
**/
-export async function isSessionValid({ serviceUrl, session }: { serviceUrl: string; session: Session }): Promise {
+export async function isSessionValid({ serviceConfig, session }: { serviceConfig: ServiceConfig; session: Session }): Promise {
// session can't be checked without user
if (!session.factors?.user) {
console.warn("Session has no user");
@@ -51,20 +44,14 @@ export async function isSessionValid({ serviceUrl, session }: { serviceUrl: stri
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
// Get login settings to determine if MFA is actually required by policy
- const loginSettings = await getLoginSettings({
- serviceUrl,
- organization: session.factors?.user?.organizationId,
- });
+ const loginSettings = await getLoginSettings({ serviceConfig, organization: session.factors?.user?.organizationId });
// Use the existing shouldEnforceMFA function to determine if MFA is required
const isMfaRequired = shouldEnforceMFA(session, loginSettings);
// Only enforce MFA validation if MFA is required by policy
if (isMfaRequired) {
- const authMethodTypes = await listAuthenticationMethodTypes({
- serviceUrl,
- userId: session.factors.user.id,
- });
+ const authMethodTypes = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
const authMethods = authMethodTypes.authMethodTypes;
// Filter to only MFA methods (exclude PASSWORD and PASSKEY)
@@ -134,10 +121,7 @@ export async function isSessionValid({ serviceUrl, session }: { serviceUrl: stri
// Check email verification if EMAIL_VERIFICATION environment variable is enabled
if (process.env.EMAIL_VERIFICATION === "true") {
- const userResponse = await getUserByID({
- serviceUrl,
- userId: session.factors.user.id,
- });
+ const userResponse = await getUserByID({ serviceConfig, userId: session.factors.user.id });
const humanUser = userResponse?.user?.type.case === "human" ? userResponse?.user.type.value : undefined;
@@ -151,12 +135,12 @@ export async function isSessionValid({ serviceUrl, session }: { serviceUrl: stri
}
export async function findValidSession({
- serviceUrl,
+ serviceConfig,
sessions,
authRequest,
samlRequest,
}: {
- serviceUrl: string;
+ serviceConfig: ServiceConfig;
sessions: Session[];
authRequest?: AuthRequest;
samlRequest?: SAMLRequest;
@@ -189,7 +173,7 @@ export async function findValidSession({
// return the first valid session according to settings
for (const session of sessionsWithHint) {
- if (await isSessionValid({ serviceUrl, session })) {
+ if (await isSessionValid({ serviceConfig, session })) {
return session;
}
}
diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts
index 161ed402a..1a80dd49c 100644
--- a/apps/login/src/lib/verify-helper.ts
+++ b/apps/login/src/lib/verify-helper.ts
@@ -8,7 +8,7 @@ import crypto from "crypto";
import moment from "moment";
import { cookies } from "next/headers";
import { getFingerprintIdCookie } from "./fingerprint";
-import { getUserByID } from "./zitadel";
+import { getUserByID, ServiceConfig } from "./zitadel";
export function checkPasswordChangeRequired(
expirySettings: PasswordExpirySettings | undefined,
@@ -82,7 +82,7 @@ export function checkEmailVerification(session: Session, humanUser?: HumanUser,
}
export async function checkMFAFactors(
- serviceUrl: string,
+ serviceConfig: ServiceConfig,
session: Session,
loginSettings: LoginSettings | undefined,
authMethods: AuthenticationMethodType[],
@@ -179,10 +179,7 @@ export async function checkMFAFactors(
session?.factors?.user?.id &&
shouldEnforceMFA(session, loginSettings)
) {
- const userResponse = await getUserByID({
- serviceUrl,
- userId: session.factors?.user?.id,
- });
+ const userResponse = await getUserByID({ serviceConfig, userId: session.factors?.user?.id });
const humanUser = userResponse?.user?.type.case === "human" ? userResponse?.user.type.value : undefined;
diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts
index 8185f49c9..72d2ab12f 100644
--- a/apps/login/src/lib/zitadel.ts
+++ b/apps/login/src/lib/zitadel.ts
@@ -45,15 +45,14 @@ async function cacheWrapper(callback: Promise) {
}
export async function getHostedLoginTranslation({
- serviceUrl,
+ serviceConfig,
organization,
locale,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
organization?: string;
locale?: string;
-}) {
- const settingsService: Client = await createServiceForHost(SettingsService, serviceUrl);
+}>) {
+ const settingsService: Client = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getHostedLoginTranslation(
@@ -78,8 +77,13 @@ export async function getHostedLoginTranslation({
return useCache ? cacheWrapper(callback) : callback;
}
-export async function getBrandingSettings({ serviceUrl, organization }: { serviceUrl: string; organization?: string }) {
- const settingsService: Client = await createServiceForHost(SettingsService, serviceUrl);
+export async function getBrandingSettings({
+ serviceConfig,
+ organization,
+}: WithServiceConfig<{
+ organization?: string;
+}>) {
+ const settingsService: Client = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getBrandingSettings({ ctx: makeReqCtx(organization) }, {})
@@ -88,8 +92,13 @@ export async function getBrandingSettings({ serviceUrl, organization }: { servic
return useCache ? cacheWrapper(callback) : callback;
}
-export async function getLoginSettings({ serviceUrl, organization }: { serviceUrl: string; organization?: string }) {
- const settingsService: Client = await createServiceForHost(SettingsService, serviceUrl);
+export async function getLoginSettings({
+ serviceConfig,
+ organization,
+}: WithServiceConfig<{
+ organization?: string;
+}>) {
+ const settingsService: Client = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getLoginSettings({ ctx: makeReqCtx(organization) }, {})
@@ -98,16 +107,16 @@ export async function getLoginSettings({ serviceUrl, organization }: { serviceUr
return useCache ? cacheWrapper(callback) : callback;
}
-export async function getSecuritySettings({ serviceUrl }: { serviceUrl: string }) {
- const settingsService: Client = await createServiceForHost(SettingsService, serviceUrl);
+export async function getSecuritySettings({ serviceConfig }: WithServiceConfig) {
+ const settingsService: Client = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService.getSecuritySettings({}).then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
-export async function getLockoutSettings({ serviceUrl, orgId }: { serviceUrl: string; orgId?: string }) {
- const settingsService: Client = await createServiceForHost(SettingsService, serviceUrl);
+export async function getLockoutSettings({ serviceConfig, orgId }: WithServiceConfig<{ orgId?: string }>) {
+ const settingsService: Client = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getLockoutSettings({ ctx: makeReqCtx(orgId) }, {})
@@ -116,8 +125,8 @@ export async function getLockoutSettings({ serviceUrl, orgId }: { serviceUrl: st
return useCache ? cacheWrapper(callback) : callback;
}
-export async function getPasswordExpirySettings({ serviceUrl, orgId }: { serviceUrl: string; orgId?: string }) {
- const settingsService: Client = await createServiceForHost(SettingsService, serviceUrl);
+export async function getPasswordExpirySettings({ serviceConfig, orgId }: WithServiceConfig<{ orgId?: string }>) {
+ const settingsService: Client = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getPasswordExpirySettings({ ctx: makeReqCtx(orgId) }, {})
@@ -126,32 +135,32 @@ export async function getPasswordExpirySettings({ serviceUrl, orgId }: { service
return useCache ? cacheWrapper(callback) : callback;
}
-export async function listIDPLinks({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function listIDPLinks({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.listIDPLinks({ userId }, {});
}
-export async function addOTPEmail({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function addOTPEmail({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.addOTPEmail({ userId }, {});
}
-export async function addOTPSMS({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function addOTPSMS({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.addOTPSMS({ userId }, {});
}
-export async function registerTOTP({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function registerTOTP({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.registerTOTP({ userId }, {});
}
-export async function getGeneralSettings({ serviceUrl }: { serviceUrl: string }) {
- const settingsService: Client = await createServiceForHost(SettingsService, serviceUrl);
+export async function getGeneralSettings({ serviceConfig }: WithServiceConfig) {
+ const settingsService: Client = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService.getGeneralSettings({}, {}).then((resp) => resp.supportedLanguages);
@@ -159,13 +168,12 @@ export async function getGeneralSettings({ serviceUrl }: { serviceUrl: string })
}
export async function getLegalAndSupportSettings({
- serviceUrl,
+ serviceConfig,
organization,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
organization?: string;
-}) {
- const settingsService: Client = await createServiceForHost(SettingsService, serviceUrl);
+}>) {
+ const settingsService: Client = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getLegalAndSupportSettings({ ctx: makeReqCtx(organization) }, {})
@@ -175,13 +183,12 @@ export async function getLegalAndSupportSettings({
}
export async function getPasswordComplexitySettings({
- serviceUrl,
+ serviceConfig,
organization,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
organization?: string;
-}) {
- const settingsService: Client = await createServiceForHost(SettingsService, serviceUrl);
+}>) {
+ const settingsService: Client = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getPasswordComplexitySettings({ ctx: makeReqCtx(organization) })
@@ -191,15 +198,14 @@ export async function getPasswordComplexitySettings({
}
export async function createSessionFromChecks({
- serviceUrl,
+ serviceConfig,
checks,
lifetime,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
checks: Checks;
lifetime: Duration;
-}) {
- const sessionService: Client = await createServiceForHost(SessionService, serviceUrl);
+}>) {
+ const sessionService: Client = await createServiceForHost(SessionService, serviceConfig);
const userAgent = await getUserAgent();
@@ -207,21 +213,20 @@ export async function createSessionFromChecks({
}
export async function createSessionForUserIdAndIdpIntent({
- serviceUrl,
+ serviceConfig,
userId,
idpIntent,
lifetime,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
userId: string;
idpIntent: {
idpIntentId?: string | undefined;
idpIntentToken?: string | undefined;
};
lifetime: Duration;
-}) {
+}>) {
console.log("Creating session for userId and IDP intent", { userId, idpIntent, lifetime });
- const sessionService: Client = await createServiceForHost(SessionService, serviceUrl);
+ const sessionService: Client = await createServiceForHost(SessionService, serviceConfig);
const userAgent = await getUserAgent();
@@ -241,21 +246,20 @@ export async function createSessionForUserIdAndIdpIntent({
}
export async function setSession({
- serviceUrl,
+ serviceConfig,
sessionId,
sessionToken,
challenges,
checks,
lifetime,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
sessionId: string;
sessionToken: string;
challenges: RequestChallenges | undefined;
checks?: Checks;
lifetime: Duration;
-}) {
- const sessionService: Client = await createServiceForHost(SessionService, serviceUrl);
+}>) {
+ const sessionService: Client = await createServiceForHost(SessionService, serviceConfig);
return sessionService.setSession(
{
@@ -271,40 +275,37 @@ export async function setSession({
}
export async function getSession({
- serviceUrl,
+ serviceConfig,
sessionId,
sessionToken,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
sessionId: string;
sessionToken: string;
-}) {
- const sessionService: Client = await createServiceForHost(SessionService, serviceUrl);
+}>) {
+ const sessionService: Client = await createServiceForHost(SessionService, serviceConfig);
return sessionService.getSession({ sessionId, sessionToken }, {});
}
export async function deleteSession({
- serviceUrl,
+ serviceConfig,
sessionId,
sessionToken,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
sessionId: string;
sessionToken: string;
-}) {
- const sessionService: Client = await createServiceForHost(SessionService, serviceUrl);
+}>) {
+ const sessionService: Client = await createServiceForHost(SessionService, serviceConfig);
return sessionService.deleteSession({ sessionId, sessionToken }, {});
}
-type ListSessionsCommand = {
- serviceUrl: string;
+type ListSessionsCommand = WithServiceConfig<{
ids: string[];
-};
+}>;
-export async function listSessions({ serviceUrl, ids }: ListSessionsCommand) {
- const sessionService: Client = await createServiceForHost(SessionService, serviceUrl);
+export async function listSessions({ serviceConfig, ids }: ListSessionsCommand) {
+ const sessionService: Client = await createServiceForHost(SessionService, serviceConfig);
return sessionService.listSessions(
{
@@ -321,17 +322,16 @@ export async function listSessions({ serviceUrl, ids }: ListSessionsCommand) {
);
}
-export type AddHumanUserData = {
- serviceUrl: string;
+export type AddHumanUserData = WithServiceConfig<{
firstName: string;
lastName: string;
email: string;
password?: string;
organization: string;
-};
+}>;
-export async function addHumanUser({ serviceUrl, email, firstName, lastName, password, organization }: AddHumanUserData) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function addHumanUser({ serviceConfig, email, firstName, lastName, password, organization }: AddHumanUserData) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
let addHumanUserRequest: AddHumanUserRequest = create(AddHumanUserRequestSchema, {
email: {
@@ -360,67 +360,69 @@ export async function addHumanUser({ serviceUrl, email, firstName, lastName, pas
return userService.addHumanUser(addHumanUserRequest);
}
-export async function addHuman({ serviceUrl, request }: { serviceUrl: string; request: AddHumanUserRequest }) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function addHuman({ serviceConfig, request }: WithServiceConfig<{ request: AddHumanUserRequest }>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.addHumanUser(request);
}
-export async function updateHuman({ serviceUrl, request }: { serviceUrl: string; request: UpdateHumanUserRequest }) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function updateHuman({
+ serviceConfig,
+ request,
+}: WithServiceConfig<{
+ request: UpdateHumanUserRequest;
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.updateHumanUser(request);
}
export async function verifyTOTPRegistration({
- serviceUrl,
+ serviceConfig,
code,
userId,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
code: string;
userId: string;
-}) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.verifyTOTPRegistration({ code, userId }, {});
}
-export async function getUserByID({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function getUserByID({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.getUserByID({ userId }, {});
}
-export async function humanMFAInitSkipped({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function humanMFAInitSkipped({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.humanMFAInitSkipped({ userId }, {});
}
export async function verifyInviteCode({
- serviceUrl,
+ serviceConfig,
userId,
verificationCode,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
userId: string;
verificationCode: string;
-}) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.verifyInviteCode({ userId, verificationCode }, {});
}
export async function sendEmailCode({
- serviceUrl,
+ serviceConfig,
userId,
urlTemplate,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
userId: string;
urlTemplate: string;
-}) {
+}>) {
let medium = create(SendEmailCodeRequestSchema, { userId });
medium = create(SendEmailCodeRequestSchema, {
@@ -433,20 +435,19 @@ export async function sendEmailCode({
},
});
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.sendEmailCode(medium, {});
}
export async function createInviteCode({
- serviceUrl,
+ serviceConfig,
urlTemplate,
userId,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
urlTemplate: string;
userId: string;
-}) {
+}>) {
let medium = create(SendInviteCodeSchema, {
applicationName: process.env.NEXT_PUBLIC_APPLICATION_NAME || "Zitadel Login",
});
@@ -456,7 +457,7 @@ export async function createInviteCode({
urlTemplate,
};
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.createInviteCode(
{
@@ -470,16 +471,15 @@ export async function createInviteCode({
);
}
-export type ListUsersCommand = {
- serviceUrl: string;
+export type ListUsersCommand = WithServiceConfig<{
loginName?: string;
userName?: string;
email?: string;
phone?: string;
organizationId?: string;
-};
+}>;
-export async function listUsers({ serviceUrl, loginName, userName, phone, email, organizationId }: ListUsersCommand) {
+export async function listUsers({ serviceConfig, loginName, userName, phone, email, organizationId }: ListUsersCommand) {
const queries: SearchQuery[] = [];
// either use loginName or userName, email, phone
@@ -562,18 +562,17 @@ export async function listUsers({ serviceUrl, loginName, userName, phone, email,
);
}
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.listUsers({ queries });
}
-export type SearchUsersCommand = {
- serviceUrl: string;
+export type SearchUsersCommand = WithServiceConfig<{
searchValue: string;
loginSettings: LoginSettings;
organizationId?: string;
suffix?: string;
-};
+}>;
const PhoneQuery = (searchValue: string) =>
create(SearchQuerySchema, {
@@ -612,7 +611,13 @@ const EmailQuery = (searchValue: string) =>
* this is a dedicated search function to search for users from the loginname page
* it searches users based on the loginName or userName and org suffix combination, and falls back to email and phone if no users are found
* */
-export async function searchUsers({ serviceUrl, searchValue, loginSettings, organizationId, suffix }: SearchUsersCommand) {
+export async function searchUsers({
+ serviceConfig,
+ searchValue,
+ loginSettings,
+ organizationId,
+ suffix,
+}: SearchUsersCommand) {
const queries: SearchQuery[] = [];
const t = await getTranslations("zitadel");
@@ -640,7 +645,7 @@ export async function searchUsers({ serviceUrl, searchValue, loginSettings, orga
);
}
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
const loginNameResult = await userService.listUsers({ queries });
@@ -723,8 +728,8 @@ export async function searchUsers({ serviceUrl, searchValue, loginSettings, orga
return { result: [] };
}
-export async function getDefaultOrg({ serviceUrl }: { serviceUrl: string }): Promise {
- const orgService: Client = await createServiceForHost(OrganizationService, serviceUrl);
+export async function getDefaultOrg({ serviceConfig }: WithServiceConfig): Promise {
+ const orgService: Client = await createServiceForHost(OrganizationService, serviceConfig);
return orgService
.listOrganizations(
@@ -743,8 +748,8 @@ export async function getDefaultOrg({ serviceUrl }: { serviceUrl: string }): Pro
.then((resp) => (resp?.result && resp.result[0] ? resp.result[0] : null));
}
-export async function getOrgsByDomain({ serviceUrl, domain }: { serviceUrl: string; domain: string }) {
- const orgService: Client = await createServiceForHost(OrganizationService, serviceUrl);
+export async function getOrgsByDomain({ serviceConfig, domain }: WithServiceConfig<{ domain: string }>) {
+ const orgService: Client = await createServiceForHost(OrganizationService, serviceConfig);
return orgService.listOrganizations(
{
@@ -762,15 +767,15 @@ export async function getOrgsByDomain({ serviceUrl, domain }: { serviceUrl: stri
}
export async function startIdentityProviderFlow({
- serviceUrl,
+ serviceConfig,
idpId,
urls,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
idpId: string;
urls: RedirectURLsJson;
-}): Promise {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+}>): Promise {
+ // Use empty publicHost to avoid issues with redirect URIs pointing to the login UI instead of the zitadel API
+ const userService: Client = await createServiceForHost(UserService, {...serviceConfig, publicHost: ''});
return userService
.startIdentityProviderIntent({
@@ -823,17 +828,16 @@ export async function startIdentityProviderFlow({
}
export async function startLDAPIdentityProviderFlow({
- serviceUrl,
+ serviceConfig,
idpId,
username,
password,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
idpId: string;
username: string;
password: string;
-}) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.startIdentityProviderIntent({
idpId,
@@ -847,16 +851,26 @@ export async function startLDAPIdentityProviderFlow({
});
}
-export async function getAuthRequest({ serviceUrl, authRequestId }: { serviceUrl: string; authRequestId: string }) {
- const oidcService = await createServiceForHost(OIDCService, serviceUrl);
+export async function getAuthRequest({
+ serviceConfig,
+ authRequestId,
+}: WithServiceConfig<{
+ authRequestId: string;
+}>) {
+ const oidcService = await createServiceForHost(OIDCService, serviceConfig);
return oidcService.getAuthRequest({
authRequestId,
});
}
-export async function getDeviceAuthorizationRequest({ serviceUrl, userCode }: { serviceUrl: string; userCode: string }) {
- const oidcService = await createServiceForHost(OIDCService, serviceUrl);
+export async function getDeviceAuthorizationRequest({
+ serviceConfig,
+ userCode,
+}: WithServiceConfig<{
+ userCode: string;
+}>) {
+ const oidcService = await createServiceForHost(OIDCService, serviceConfig);
return oidcService.getDeviceAuthorizationRequest({
userCode,
@@ -864,15 +878,14 @@ export async function getDeviceAuthorizationRequest({ serviceUrl, userCode }: {
}
export async function authorizeOrDenyDeviceAuthorization({
- serviceUrl,
+ serviceConfig,
deviceAuthorizationId,
session,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
deviceAuthorizationId: string;
session?: { sessionId: string; sessionToken: string };
-}) {
- const oidcService = await createServiceForHost(OIDCService, serviceUrl);
+}>) {
+ const oidcService = await createServiceForHost(OIDCService, serviceConfig);
return oidcService.authorizeOrDenyDeviceAuthorization({
deviceAuthorizationId,
@@ -888,36 +901,40 @@ export async function authorizeOrDenyDeviceAuthorization({
});
}
-export async function createCallback({ serviceUrl, req }: { serviceUrl: string; req: CreateCallbackRequest }) {
- const oidcService = await createServiceForHost(OIDCService, serviceUrl);
+export async function createCallback({ serviceConfig, req }: WithServiceConfig<{ req: CreateCallbackRequest }>) {
+ const oidcService = await createServiceForHost(OIDCService, serviceConfig);
return oidcService.createCallback(req);
}
-export async function getSAMLRequest({ serviceUrl, samlRequestId }: { serviceUrl: string; samlRequestId: string }) {
- const samlService = await createServiceForHost(SAMLService, serviceUrl);
+export async function getSAMLRequest({
+ serviceConfig,
+ samlRequestId,
+}: WithServiceConfig<{
+ samlRequestId: string;
+}>) {
+ const samlService = await createServiceForHost(SAMLService, serviceConfig);
return samlService.getSAMLRequest({
samlRequestId,
});
}
-export async function createResponse({ serviceUrl, req }: { serviceUrl: string; req: CreateResponseRequest }) {
- const samlService = await createServiceForHost(SAMLService, serviceUrl);
+export async function createResponse({ serviceConfig, req }: WithServiceConfig<{ req: CreateResponseRequest }>) {
+ const samlService = await createServiceForHost(SAMLService, serviceConfig);
return samlService.createResponse(req);
}
export async function verifyEmail({
- serviceUrl,
+ serviceConfig,
userId,
verificationCode,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
userId: string;
verificationCode: string;
-}) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.verifyEmail(
{
@@ -929,14 +946,13 @@ export async function verifyEmail({
}
export async function resendEmailCode({
- serviceUrl,
+ serviceConfig,
userId,
urlTemplate,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
userId: string;
urlTemplate: string;
-}) {
+}>) {
let request: ResendEmailCodeRequest = create(ResendEmailCodeRequestSchema, {
userId,
});
@@ -947,33 +963,42 @@ export async function resendEmailCode({
request = { ...request, verification: { case: "sendCode", value: medium } };
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.resendEmailCode(request, {});
}
-export async function retrieveIDPIntent({ serviceUrl, id, token }: { serviceUrl: string; id: string; token: string }) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function retrieveIDPIntent({
+ serviceConfig,
+ id,
+ token,
+}: WithServiceConfig<{
+ id: string;
+ token: string;
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.retrieveIdentityProviderIntent({ idpIntentId: id, idpIntentToken: token }, {});
}
-export async function getIDPByID({ serviceUrl, id }: { serviceUrl: string; id: string }) {
- const idpService: Client = await createServiceForHost(IdentityProviderService, serviceUrl);
+export async function getIDPByID({ serviceConfig, id }: WithServiceConfig<{ id: string }>) {
+ const idpService: Client = await createServiceForHost(
+ IdentityProviderService,
+ serviceConfig,
+ );
return idpService.getIDPByID({ id }, {}).then((resp) => resp.idp);
}
export async function addIDPLink({
- serviceUrl,
+ serviceConfig,
idp,
userId,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
idp: { id: string; userId: string; userName: string };
userId: string;
-}) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.addIDPLink(
{
@@ -989,14 +1014,13 @@ export async function addIDPLink({
}
export async function passwordReset({
- serviceUrl,
+ serviceConfig,
userId,
urlTemplate,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
userId: string;
urlTemplate?: string;
-}) {
+}>) {
let medium = create(SendPasswordResetLinkSchema, {
notificationType: NotificationType.Email,
});
@@ -1006,7 +1030,7 @@ export async function passwordReset({
urlTemplate,
};
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.passwordReset(
{
@@ -1021,16 +1045,15 @@ export async function passwordReset({
}
export async function setUserPassword({
- serviceUrl,
+ serviceConfig,
userId,
password,
code,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
userId: string;
password: string;
code?: string;
-}) {
+}>) {
let payload = create(SetPasswordRequestSchema, {
userId,
newPassword: {
@@ -1048,7 +1071,7 @@ export async function setUserPassword({
};
}
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.setPassword(payload, {}).catch((error) => {
// throw error if failed precondition (ex. User is not yet initialized)
@@ -1060,8 +1083,13 @@ export async function setUserPassword({
});
}
-export async function setPassword({ serviceUrl, payload }: { serviceUrl: string; payload: SetPasswordRequest }) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function setPassword({
+ serviceConfig,
+ payload,
+}: WithServiceConfig<{
+ payload: SetPasswordRequest;
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.setPassword(payload, {});
}
@@ -1072,8 +1100,13 @@ export async function setPassword({ serviceUrl, payload }: { serviceUrl: string;
* @param userId the id of the user where the email should be set
* @returns the newly set email
*/
-export async function createPasskeyRegistrationLink({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function createPasskeyRegistrationLink({
+ serviceConfig,
+ userId,
+}: WithServiceConfig<{
+ userId: string;
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.createPasskeyRegistrationLink({
userId,
@@ -1091,8 +1124,15 @@ export async function createPasskeyRegistrationLink({ serviceUrl, userId }: { se
* @param domain the domain on which the factor is registered
* @returns the newly set email
*/
-export async function registerU2F({ serviceUrl, userId, domain }: { serviceUrl: string; userId: string; domain: string }) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function registerU2F({
+ serviceConfig,
+ userId,
+ domain,
+}: WithServiceConfig<{
+ userId: string;
+ domain: string;
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.registerU2F({
userId,
@@ -1107,13 +1147,12 @@ export async function registerU2F({ serviceUrl, userId, domain }: { serviceUrl:
* @returns the result of the verification
*/
export async function verifyU2FRegistration({
- serviceUrl,
+ serviceConfig,
request,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
request: VerifyU2FRegistrationRequest;
-}) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.verifyU2FRegistration(request, {});
}
@@ -1126,19 +1165,18 @@ export async function verifyU2FRegistration({
* @returns the active identity providers
*/
export async function getActiveIdentityProviders({
- serviceUrl,
+ serviceConfig,
orgId,
linking_allowed,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
orgId?: string;
linking_allowed?: boolean;
-}) {
+}>) {
const props: any = { ctx: makeReqCtx(orgId) };
if (linking_allowed) {
props.linkingAllowed = linking_allowed;
}
- const settingsService: Client = await createServiceForHost(SettingsService, serviceUrl);
+ const settingsService: Client = await createServiceForHost(SettingsService, serviceConfig);
return settingsService.getActiveIdentityProviders(props, {});
}
@@ -1150,13 +1188,12 @@ export async function getActiveIdentityProviders({
* @returns the result of the verification
*/
export async function verifyPasskeyRegistration({
- serviceUrl,
+ serviceConfig,
request,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
request: VerifyPasskeyRegistrationRequest;
-}) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.verifyPasskeyRegistration(request, {});
}
@@ -1170,17 +1207,16 @@ export async function verifyPasskeyRegistration({
* @returns the newly set email
*/
export async function registerPasskey({
- serviceUrl,
+ serviceConfig,
userId,
code,
domain,
-}: {
- serviceUrl: string;
+}: WithServiceConfig<{
userId: string;
code: { id: string; code: string };
domain: string;
-}) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.registerPasskey({
userId,
@@ -1195,33 +1231,64 @@ export async function registerPasskey({
* @param userId the id of the user where the email should be set
* @returns the list of authentication method types
*/
-export async function listAuthenticationMethodTypes({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
- const userService: Client = await createServiceForHost(UserService, serviceUrl);
+export async function listAuthenticationMethodTypes({
+ serviceConfig,
+ userId,
+}: WithServiceConfig<{
+ userId: string;
+}>) {
+ const userService: Client = await createServiceForHost(UserService, serviceConfig);
return userService.listAuthenticationMethodTypes({
userId,
});
}
-export function createServerTransport(token: string, baseUrl: string) {
+export interface ServiceConfig {
+ baseUrl: string;
+ instanceHost?: string; // only for multi-tenant
+ publicHost?: string; // only for multi-tenant
+}
+
+/**
+ * Base type that all function parameters must extend to ensure serviceConfig is always required
+ */
+export type WithServiceConfig = T & {
+ serviceConfig: ServiceConfig;
+};
+
+export function createServerTransport(token: string, serviceConfig: ServiceConfig) {
return libCreateServerTransport(token, {
- baseUrl,
- interceptors: !process.env.CUSTOM_REQUEST_HEADERS
- ? undefined
- : [
- (next) => {
- return (req) => {
- process.env.CUSTOM_REQUEST_HEADERS!.split(",").forEach((header) => {
- const kv = header.indexOf(":");
- if (kv > 0) {
- req.header.set(header.slice(0, kv).trim(), header.slice(kv + 1).trim());
- } else {
- console.warn(`Skipping malformed header: ${header}`);
+ baseUrl: serviceConfig.baseUrl,
+ interceptors:
+ !process.env.CUSTOM_REQUEST_HEADERS && !serviceConfig.instanceHost && !serviceConfig.publicHost
+ ? undefined
+ : [
+ (next) => {
+ return (req) => {
+ // Apply headers from serviceConfig
+ if (serviceConfig.instanceHost) {
+ req.header.set("x-zitadel-instance-host", serviceConfig.instanceHost);
}
- });
- return next(req);
- };
- },
- ],
+ if (serviceConfig.publicHost) {
+ req.header.set("x-zitadel-public-host", serviceConfig.publicHost);
+ }
+
+ // Apply headers from CUSTOM_REQUEST_HEADERS environment variable
+ if (process.env.CUSTOM_REQUEST_HEADERS) {
+ process.env.CUSTOM_REQUEST_HEADERS.split(",").forEach((header) => {
+ const kv = header.indexOf(":");
+ if (kv > 0) {
+ req.header.set(header.slice(0, kv).trim(), header.slice(kv + 1).trim());
+ } else {
+ console.warn(`Skipping malformed header: ${header}`);
+ }
+ });
+ }
+
+ return next(req);
+ };
+ },
+ ],
});
}
diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts
index 8eca00510..7a9cc7f8f 100644
--- a/apps/login/src/middleware.ts
+++ b/apps/login/src/middleware.ts
@@ -3,28 +3,16 @@ import { SecuritySettings } from "@zitadel/proto/zitadel/settings/v2/security_se
import { headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import { DEFAULT_CSP } from "../constants/csp";
-import { getServiceUrlFromHeaders } from "./lib/service-url";
+import { getServiceConfig } from "./lib/service-url";
export const config = {
- matcher: [
- "/.well-known/:path*",
- "/oauth/:path*",
- "/oidc/:path*",
- "/idps/callback/:path*",
- "/saml/:path*",
- "/:path*",
- ],
+ matcher: ["/.well-known/:path*", "/oauth/:path*", "/oidc/:path*", "/idps/callback/:path*", "/saml/:path*", "/:path*"],
};
-async function loadSecuritySettings(
- request: NextRequest,
-): Promise {
+async function loadSecuritySettings(request: NextRequest): Promise {
const securityResponse = await fetch(`${request.nextUrl.origin}/security`);
if (!securityResponse.ok) {
- console.error(
- "Failed to fetch security settings:",
- securityResponse.statusText,
- );
+ console.error("Failed to fetch security settings:", securityResponse.statusText);
return null;
}
@@ -49,40 +37,28 @@ export async function middleware(request: NextRequest) {
}
// Only run the rest of the logic for the original matcher paths
- const proxyPaths = [
- "/.well-known/",
- "/oauth/",
- "/oidc/",
- "/idps/callback/",
- "/saml/",
- ];
+ const proxyPaths = ["/.well-known/", "/oauth/", "/oidc/", "/idps/callback/", "/saml/"];
- const isMatched = proxyPaths.some((prefix) =>
- request.nextUrl.pathname.startsWith(prefix),
- );
+ const isMatched = proxyPaths.some((prefix) => request.nextUrl.pathname.startsWith(prefix));
- // escape proxy if the environment is setup for multitenancy
- if (
- !isMatched ||
- !process.env.ZITADEL_API_URL ||
- !process.env.ZITADEL_SERVICE_USER_TOKEN
- ) {
- // For all other routes, just add the header and continue
+ // Only proxy in self-hosted mode
+ if (!isMatched) {
+ // For multi-tenant or non-proxied routes, just add the header and continue
return NextResponse.next({
request: { headers: requestHeaders },
});
}
const _headers = await headers();
- const { serviceUrl } = getServiceUrlFromHeaders(_headers);
+ const { serviceConfig } = getServiceConfig(_headers);
- const instanceHost = `${serviceUrl}`
- .replace("https://", "")
- .replace("http://", "");
-
- // Add additional headers as before
- requestHeaders.set("x-zitadel-public-host", `${request.nextUrl.host}`);
- requestHeaders.set("x-zitadel-instance-host", instanceHost);
+ // add additional headers if available
+ if (serviceConfig.publicHost) {
+ requestHeaders.set("x-zitadel-public-host", serviceConfig.publicHost);
+ }
+ if (serviceConfig.instanceHost) {
+ requestHeaders.set("x-zitadel-instance-host", serviceConfig.instanceHost);
+ }
const responseHeaders = new Headers();
responseHeaders.set("Access-Control-Allow-Origin", "*");
@@ -98,7 +74,7 @@ export async function middleware(request: NextRequest) {
responseHeaders.delete("X-Frame-Options");
}
- request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`;
+ request.nextUrl.href = `${serviceConfig.baseUrl}${request.nextUrl.pathname}${request.nextUrl.search}`;
return NextResponse.rewrite(request.nextUrl, {
request: {
diff --git a/apps/login/tsconfig.json b/apps/login/tsconfig.json
index dbb65389d..d0403e237 100644
--- a/apps/login/tsconfig.json
+++ b/apps/login/tsconfig.json
@@ -28,6 +28,16 @@
]
}
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules", "integration", "acceptance"]
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules",
+ "integration",
+ "acceptance"
+ ]
}
diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go
index 152176a59..bfbcd29f7 100644
--- a/internal/api/oidc/auth_request.go
+++ b/internal/api/oidc/auth_request.go
@@ -10,10 +10,11 @@ import (
"strings"
"time"
+ "github.com/go-jose/go-jose/v4"
"github.com/zitadel/logging"
+ "github.com/zitadel/oidc/v3/pkg/crypto"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
- "golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http"
@@ -33,6 +34,7 @@ const (
LoginPostLogoutRedirectParam = "post_logout_redirect"
LoginLogoutHintParam = "logout_hint"
LoginUILocalesParam = "ui_locales"
+ LoginLogoutTokenParam = "logout_token"
LoginPath = "/login"
LogoutPath = "/logout"
LogoutDonePath = "/logout/done"
@@ -298,7 +300,11 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR
} else {
logoutURI = logoutURI.JoinPath(LogoutPath)
}
- return buildLoginV2LogoutURL(logoutURI, redirectURI, endSessionRequest.LogoutHint, endSessionRequest.UILocales), nil
+ signer, _, err := GetSignerOnce(o.query.GetActiveSigningWebKey)(ctx)
+ if err != nil {
+ return "", err
+ }
+ return buildLoginV2LogoutURL(logoutURI, redirectURI, endSessionRequest.LogoutHint, endSessionRequest.UILocales, signer)
}
// V1:
@@ -375,7 +381,13 @@ func (o *OPStorage) federatedLogout(ctx context.Context, sessionID string, postL
return login.ExternalLogoutPath(sessionID)
}
-func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, uiLocales []language.Tag) string {
+type logoutTokenPayload struct {
+ PostLogoutRedirectURI string `json:"post_logout_redirect_uri,omitempty"`
+ LogoutHint string `json:"logout_hint,omitempty"`
+ UILocales oidc.Locales `json:"ui_locales,omitempty"`
+}
+
+func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, uiLocales oidc.Locales, signer jose.Signer) (string, error) {
if strings.HasSuffix(logoutURI.Path, "/") && len(logoutURI.Path) > 1 {
logoutURI.Path = strings.TrimSuffix(logoutURI.Path, "/")
}
@@ -386,14 +398,19 @@ func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, u
q.Set(LoginLogoutHintParam, logoutHint)
}
if len(uiLocales) > 0 {
- locales := make([]string, len(uiLocales))
- for i, locale := range uiLocales {
- locales[i] = locale.String()
- }
- q.Set(LoginUILocalesParam, strings.Join(locales, " "))
+ q.Set(LoginUILocalesParam, uiLocales.String())
}
+ logoutToken, err := crypto.Sign(&logoutTokenPayload{
+ PostLogoutRedirectURI: redirectURI,
+ LogoutHint: logoutHint,
+ UILocales: uiLocales,
+ }, signer)
+ if err != nil {
+ return "", err
+ }
+ q.Set(LoginLogoutTokenParam, logoutToken)
logoutURI.RawQuery = q.Encode()
- return logoutURI.String()
+ return logoutURI.String(), nil
}
// v2PostLogoutRedirectURI will take care that the post_logout_redirect_uri is correctly set for v2 logins.
diff --git a/internal/api/oidc/auth_request_test.go b/internal/api/oidc/auth_request_test.go
index 0210ead49..cf823657b 100644
--- a/internal/api/oidc/auth_request_test.go
+++ b/internal/api/oidc/auth_request_test.go
@@ -1,9 +1,13 @@
package oidc
import (
+ "crypto/rand"
+ "crypto/rsa"
+ "encoding/json"
"net/url"
"testing"
+ "github.com/go-jose/go-jose/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
@@ -18,6 +22,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
redirectURI string
logoutHint string
uiLocales []language.Tag
+ signer jose.Signer
expectedParams map[string]string
}{
{
@@ -26,6 +31,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
redirectURI: "https://client/cb",
expectedParams: map[string]string{
"post_logout_redirect": "https://client/cb",
+ "logout_token": "", // presence checked separately
},
},
{
@@ -36,6 +42,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
expectedParams: map[string]string{
"post_logout_redirect": "https://client/cb",
"logout_hint": "user@example.com",
+ "logout_token": "", // presence checked separately
},
},
{
@@ -46,6 +53,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
expectedParams: map[string]string{
"post_logout_redirect": "https://client/cb",
"ui_locales": "en it",
+ "logout_token": "", // presence checked separately
},
},
{
@@ -58,6 +66,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
"post_logout_redirect": "https://client/cb",
"logout_hint": "logoutme",
"ui_locales": "de-CH fr",
+ "logout_token": "", // presence checked separately
},
},
{
@@ -66,6 +75,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
redirectURI: "https://client/cb",
expectedParams: map[string]string{
"post_logout_redirect": "https://client/cb",
+ "logout_token": "", // presence checked separately
},
},
}
@@ -79,9 +89,10 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
require.NoError(t, err)
// When
- got := buildLoginV2LogoutURL(logoutURI, tc.redirectURI, tc.logoutHint, tc.uiLocales)
+ got, err := buildLoginV2LogoutURL(logoutURI, tc.redirectURI, tc.logoutHint, tc.uiLocales, signer)
// Then
+ require.NoError(t, err)
gotURL, err := url.Parse(got)
require.NoError(t, err)
require.NotContains(t, gotURL.String(), "/logout/")
@@ -91,8 +102,39 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
require.Len(t, q, len(tc.expectedParams))
for k, v := range tc.expectedParams {
+ if k == LoginLogoutTokenParam {
+ assertLogoutToken(t, q.Get(k), &logoutTokenPayload{
+ PostLogoutRedirectURI: tc.redirectURI,
+ LogoutHint: tc.logoutHint,
+ UILocales: tc.uiLocales,
+ })
+ continue
+ }
assert.Equal(t, v, q.Get(k))
}
})
}
}
+
+func assertLogoutToken(t *testing.T, token string, payload *logoutTokenPayload) {
+ signature, err := jose.ParseSigned(token, []jose.SignatureAlgorithm{jose.RS256})
+ require.NoError(t, err)
+ logoutToken := new(logoutTokenPayload)
+ err = json.Unmarshal(signature.UnsafePayloadWithoutVerification(), logoutToken)
+ require.NoError(t, err)
+ assert.Equal(t, payload, logoutToken)
+}
+
+var (
+ privKey, _ = rsa.GenerateKey(rand.Reader, 2048)
+ signer = func() jose.Signer {
+ signer, _ := jose.NewSigner(
+ jose.SigningKey{
+ Algorithm: jose.RS256,
+ Key: privKey,
+ },
+ (&jose.SignerOptions{}).WithType("JWT"),
+ )
+ return signer
+ }()
+)
diff --git a/internal/api/oidc/integration_test/auth_request_test.go b/internal/api/oidc/integration_test/auth_request_test.go
index 77e389f7b..75c436464 100644
--- a/internal/api/oidc/integration_test/auth_request_test.go
+++ b/internal/api/oidc/integration_test/auth_request_test.go
@@ -551,15 +551,16 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) {
require.NoError(t, err)
}
-func buildLogoutURL(origin, logoutURLV2 string, redirectURI string, extraParams map[string]string) string {
+func buildLogoutURL(origin, logoutURLV2, redirectURI string, extraParams map[string]string) *url.URL {
u, _ := url.Parse(origin + logoutURLV2 + redirectURI)
q := u.Query()
for k, v := range extraParams {
q.Set(k, v)
}
+ q.Set("logout_token", "signed-logout-token") // placeholder
u.RawQuery = q.Encode()
// Append the redirect URI as a URL-escaped string
- return u.String()
+ return u
}
func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
@@ -567,7 +568,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
name string
clientID string
authRequestID func(t testing.TB, instance *integration.Instance, clientID, redirectURI string, scope ...string) string
- logoutURL string
+ logoutURL *url.URL
}{
{
name: "login header",
@@ -614,7 +615,18 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state", "hint", oidc.ParseLocales([]string{"it-IT", "en-US"}))
require.NoError(t, err)
- assert.Equal(t, tt.logoutURL, postLogoutRedirect.String())
+
+ requiredQueries := tt.logoutURL.Query()
+ for key, value := range requiredQueries {
+ if key == "logout_token" {
+ assert.NotEmpty(t, value[0], "logout_token must be present")
+ continue
+ }
+ assert.Equal(t, value[0], postLogoutRedirect.Query().Get(key))
+ }
+ requiredURLWithoutQueries := *tt.logoutURL
+ requiredURLWithoutQueries.RawQuery = ""
+ assert.Equal(t, requiredURLWithoutQueries.String(), postLogoutRedirect.Scheme+"://"+postLogoutRedirect.Host+postLogoutRedirect.Path)
// userinfo must not fail until login UI terminated session
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
diff --git a/packages/zitadel-client/src/node.ts b/packages/zitadel-client/src/node.ts
index 0b15310d2..c5b942a30 100644
--- a/packages/zitadel-client/src/node.ts
+++ b/packages/zitadel-client/src/node.ts
@@ -1,5 +1,14 @@
-import { createGrpcTransport, GrpcTransportOptions } from "@connectrpc/connect-node";
-import { importPKCS8, SignJWT } from "jose";
+import {
+ createGrpcTransport,
+ GrpcTransportOptions,
+} from "@connectrpc/connect-node";
+import {
+ createRemoteJWKSet,
+ importPKCS8,
+ jwtVerify,
+ JWTPayload,
+ SignJWT,
+} from "jose";
import { NewAuthorizationBearerInterceptor } from "./interceptors.js";
/**
@@ -7,10 +16,16 @@ import { NewAuthorizationBearerInterceptor } from "./interceptors.js";
* @param token
* @param opts
*/
-export function createServerTransport(token: string, opts: GrpcTransportOptions) {
+export function createServerTransport(
+ token: string,
+ opts: GrpcTransportOptions
+) {
return createGrpcTransport({
...opts,
- interceptors: [...(opts.interceptors || []), NewAuthorizationBearerInterceptor(token)],
+ interceptors: [
+ ...(opts.interceptors || []),
+ NewAuthorizationBearerInterceptor(token),
+ ],
});
}
@@ -34,3 +49,36 @@ export async function newSystemToken({
.setAudience(audience)
.sign(await importPKCS8(key, "RS256"));
}
+
+/**
+ * Verify a signed JWT with the given keys endpoint.
+ * @param token
+ * @param keysEndpoint
+ * @param options
+ */
+export async function verifyJwt(
+ token: string,
+ keysEndpoint: string,
+ options?: {
+ issuer?: string;
+ audience?: string;
+ instanceHost?: string;
+ publicHost?: string;
+ },
+): Promise {
+ const headers: Record = {};
+ if (options?.instanceHost) {
+ headers["x-zitadel-instance-host"] = options.instanceHost;
+ }
+ if (options?.publicHost) {
+ headers["x-zitadel-public-host"] = options.publicHost;
+ }
+ const JWKS = createRemoteJWKSet(new URL(keysEndpoint), {headers: headers});
+
+ const { payload } = await jwtVerify(token, JWKS, {
+ issuer: options?.issuer,
+ audience: options?.audience,
+ });
+
+ return payload as T & JWTPayload;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 36ab46dc1..a621126f1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -16,7 +16,7 @@ importers:
version: 2.29.7(@types/node@24.3.1)
nx:
specifier: 21.6.1
- version: 21.6.1(@swc/core@1.13.5(@swc/helpers@0.5.17))
+ version: 21.6.1(@swc/core@1.15.3(@swc/helpers@0.5.17))
sass:
specifier: ^1.64.1
version: 1.92.1
@@ -54,14 +54,14 @@ importers:
specifier: ^2.29.4
version: 2.30.1
next:
- specifier: 15.4.0-canary.86
- version: 15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1)
+ specifier: 15.5.7
+ version: 15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1)
next-intl:
specifier: ^3.25.1
- version: 3.26.5(next@15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react@19.1.0)
+ version: 3.26.5(next@15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react@19.1.0)
next-themes:
specifier: ^0.2.1
- version: 0.2.1(next@15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ version: 0.2.1(next@15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
nice-grpc:
specifier: 2.0.1
version: 2.0.1
@@ -166,8 +166,8 @@ importers:
specifier: ^8.57.0
version: 8.57.1
eslint-config-next:
- specifier: 15.4.0-canary.86
- version: 15.4.0-canary.86(eslint@8.57.1)(typescript@5.9.2)
+ specifier: 15.5.7
+ version: 15.5.7(eslint@8.57.1)(typescript@5.9.2)
eslint-config-prettier:
specifier: ^9.1.0
version: 9.1.2(eslint@8.57.1)
@@ -369,7 +369,7 @@ importers:
version: 18.0.0(eslint@8.57.1)(typescript@5.9.2)
'@angular-eslint/schematics':
specifier: 19.8.1
- version: 19.8.1(@angular-eslint/template-parser@19.8.1(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.43.0)(@typescript-eslint/utils@8.43.0(eslint@8.57.1)(typescript@5.9.2))(chokidar@4.0.3)(eslint@8.57.1)(typescript@5.9.2)
+ version: 19.8.1(@angular-eslint/template-parser@19.8.1(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.48.1)(@typescript-eslint/utils@8.48.1(eslint@8.57.1)(typescript@5.9.2))(chokidar@4.0.3)(eslint@8.57.1)(typescript@5.9.2)
'@angular-eslint/template-parser':
specifier: 19.8.1
version: 19.8.1(eslint@8.57.1)(typescript@5.9.2)
@@ -593,7 +593,7 @@ importers:
version: 5.63.1(@types/node@24.3.1)(typescript@5.9.2)
tsup:
specifier: ^8.4.0
- version: 8.5.0(@swc/core@1.13.5(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1)
+ version: 8.5.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1)
typescript:
specifier: ^5.8.3
version: 5.9.2
@@ -2423,6 +2423,9 @@ packages:
'@emnapi/runtime@1.5.0':
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
+ '@emnapi/runtime@1.7.1':
+ resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
+
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
@@ -2896,124 +2899,139 @@ packages:
'@iconify/utils@3.0.1':
resolution: {integrity: sha512-A78CUEnFGX8I/WlILxJCuIJXloL0j/OJ9PSchPAfCargEIKmUBWvvEMmKWB5oONwiUqlNt+5eRufdkLxeHIWYw==}
- '@img/sharp-darwin-arm64@0.34.3':
- resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==}
+ '@img/colour@1.0.0':
+ resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
+ engines: {node: '>=18'}
+
+ '@img/sharp-darwin-arm64@0.34.5':
+ resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
- '@img/sharp-darwin-x64@0.34.3':
- resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==}
+ '@img/sharp-darwin-x64@0.34.5':
+ resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
- '@img/sharp-libvips-darwin-arm64@1.2.0':
- resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==}
+ '@img/sharp-libvips-darwin-arm64@1.2.4':
+ resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
- '@img/sharp-libvips-darwin-x64@1.2.0':
- resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==}
+ '@img/sharp-libvips-darwin-x64@1.2.4':
+ resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
- '@img/sharp-libvips-linux-arm64@1.2.0':
- resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==}
+ '@img/sharp-libvips-linux-arm64@1.2.4':
+ resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
- '@img/sharp-libvips-linux-arm@1.2.0':
- resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==}
+ '@img/sharp-libvips-linux-arm@1.2.4':
+ resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
- '@img/sharp-libvips-linux-ppc64@1.2.0':
- resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==}
+ '@img/sharp-libvips-linux-ppc64@1.2.4':
+ resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
- '@img/sharp-libvips-linux-s390x@1.2.0':
- resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==}
+ '@img/sharp-libvips-linux-riscv64@1.2.4':
+ resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-s390x@1.2.4':
+ resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
- '@img/sharp-libvips-linux-x64@1.2.0':
- resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==}
+ '@img/sharp-libvips-linux-x64@1.2.4':
+ resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
- '@img/sharp-libvips-linuxmusl-arm64@1.2.0':
- resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==}
+ '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
+ resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
- '@img/sharp-libvips-linuxmusl-x64@1.2.0':
- resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==}
+ '@img/sharp-libvips-linuxmusl-x64@1.2.4':
+ resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
- '@img/sharp-linux-arm64@0.34.3':
- resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==}
+ '@img/sharp-linux-arm64@0.34.5':
+ resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
- '@img/sharp-linux-arm@0.34.3':
- resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==}
+ '@img/sharp-linux-arm@0.34.5':
+ resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
- '@img/sharp-linux-ppc64@0.34.3':
- resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==}
+ '@img/sharp-linux-ppc64@0.34.5':
+ resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
- '@img/sharp-linux-s390x@0.34.3':
- resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==}
+ '@img/sharp-linux-riscv64@0.34.5':
+ resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@img/sharp-linux-s390x@0.34.5':
+ resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
- '@img/sharp-linux-x64@0.34.3':
- resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==}
+ '@img/sharp-linux-x64@0.34.5':
+ resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
- '@img/sharp-linuxmusl-arm64@0.34.3':
- resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==}
+ '@img/sharp-linuxmusl-arm64@0.34.5':
+ resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
- '@img/sharp-linuxmusl-x64@0.34.3':
- resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==}
+ '@img/sharp-linuxmusl-x64@0.34.5':
+ resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
- '@img/sharp-wasm32@0.34.3':
- resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==}
+ '@img/sharp-wasm32@0.34.5':
+ resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
- '@img/sharp-win32-arm64@0.34.3':
- resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==}
+ '@img/sharp-win32-arm64@0.34.5':
+ resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
- '@img/sharp-win32-ia32@0.34.3':
- resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==}
+ '@img/sharp-win32-ia32@0.34.5':
+ resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
- '@img/sharp-win32-x64@0.34.3':
- resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==}
+ '@img/sharp-win32-x64@0.34.5':
+ resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
@@ -3501,56 +3519,56 @@ packages:
engines: {node: '>=18.14.0'}
hasBin: true
- '@next/env@15.4.0-canary.86':
- resolution: {integrity: sha512-WPrEvwqHnjeLx05ncJvqizbBJJFlQGRbxzOnL/pZWKzo19auM9x5Se87P27+E/D/d6jJS801l+thF85lfobAZQ==}
+ '@next/env@15.5.7':
+ resolution: {integrity: sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==}
- '@next/eslint-plugin-next@15.4.0-canary.86':
- resolution: {integrity: sha512-cOlp6ajA1ptiBxiProcXaNAR88O5ck3IwGJr+A5SnNKU4iTUg4nP0K5lS4Mkage+LAMIQ8dImkLR53PpebXICA==}
+ '@next/eslint-plugin-next@15.5.7':
+ resolution: {integrity: sha512-DtRU2N7BkGr8r+pExfuWHwMEPX5SD57FeA6pxdgCHODo+b/UgIgjE+rgWKtJAbEbGhVZ2jtHn4g3wNhWFoNBQQ==}
- '@next/swc-darwin-arm64@15.4.0-canary.86':
- resolution: {integrity: sha512-1ofBmzjPkmoMdM+dXvybZ/Roq8HRo0sFzcwXk7/FJNOufuwyK+QKdSpLE7pHlPR7ZREqfEMj61ONO+gAK+zOJw==}
+ '@next/swc-darwin-arm64@15.5.7':
+ resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@15.4.0-canary.86':
- resolution: {integrity: sha512-WCKSrllvwzYi4TgrSdgxKSOF2nhieeaWWOeGucn0OXy50uOAamr0HwP5OaIBCx3oRar4w66gvs4IrdTdMedeJA==}
+ '@next/swc-darwin-x64@15.5.7':
+ resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@15.4.0-canary.86':
- resolution: {integrity: sha512-8qn7DJVNFjhEIDo2ts0YCsO7g+vJjPWh8Ur8lBK3XspeX0BPsF4s+YmgidrpzRXeIfoo2uYLkkXcy/57CVDblw==}
+ '@next/swc-linux-arm64-gnu@15.5.7':
+ resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@next/swc-linux-arm64-musl@15.4.0-canary.86':
- resolution: {integrity: sha512-8MTn6N4Ja25neMLu2Bra1lqW9AWPqsYg0BVs5M/cxL0QkcN3mak/8LLX1vbzz7GigMGSA+NLwg+ol8lglfgIGA==}
+ '@next/swc-linux-arm64-musl@15.5.7':
+ resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
- '@next/swc-linux-x64-gnu@15.4.0-canary.86':
- resolution: {integrity: sha512-hIhzDwWDQHnH0M0Pzaqs1c5fa4+LHiLLEBuPJQvhBxQfH+Eh86DWiWHDCaoNiURvdRPg6uCuF2MjwptrMplEkg==}
+ '@next/swc-linux-x64-gnu@15.5.7':
+ resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@next/swc-linux-x64-musl@15.4.0-canary.86':
- resolution: {integrity: sha512-FG6SBuSeRWYMNu6tsfaZ4iDzv3BLxlpRncO2xvKKQPeUdDSQ0cehuHYnx8fRte8IOAJ3rlbRd6NXvrDarqu92Q==}
+ '@next/swc-linux-x64-musl@15.5.7':
+ resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
- '@next/swc-win32-arm64-msvc@15.4.0-canary.86':
- resolution: {integrity: sha512-3HvZo4VuyINrNYplRhvC8ILdKwi/vFDHOcTN/I4ru039TFpu2eO6VtXsLBdOdJjGslSSSBYkX+6yRrghihAZDA==}
+ '@next/swc-win32-arm64-msvc@15.5.7':
+ resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@15.4.0-canary.86':
- resolution: {integrity: sha512-UO9JzGGj7GhtSJFdI0Bl0dkIIBfgbhXLsgNVmq9Z/CsUsQB6J9RS/BMhsxfVwhO+RETk13nFpNutMAhAwcuD8w==}
+ '@next/swc-win32-x64-msvc@15.5.7':
+ resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -4933,8 +4951,8 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
- '@rushstack/eslint-patch@1.12.0':
- resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==}
+ '@rushstack/eslint-patch@1.15.0':
+ resolution: {integrity: sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==}
'@schematics/angular@20.3.4':
resolution: {integrity: sha512-aIgeRAfSJdxVfsES521vNb6Nuy+Uei7xE85hD+mQHX+62bN7vJhM/tmMFIYS7UF8d0rRnMPiGEr2Rfx6Om5iPg==}
@@ -5092,60 +5110,120 @@ packages:
cpu: [arm64]
os: [darwin]
+ '@swc/core-darwin-arm64@1.15.3':
+ resolution: {integrity: sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [darwin]
+
'@swc/core-darwin-x64@1.13.5':
resolution: {integrity: sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==}
engines: {node: '>=10'}
cpu: [x64]
os: [darwin]
+ '@swc/core-darwin-x64@1.15.3':
+ resolution: {integrity: sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [darwin]
+
'@swc/core-linux-arm-gnueabihf@1.13.5':
resolution: {integrity: sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==}
engines: {node: '>=10'}
cpu: [arm]
os: [linux]
+ '@swc/core-linux-arm-gnueabihf@1.15.3':
+ resolution: {integrity: sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg==}
+ engines: {node: '>=10'}
+ cpu: [arm]
+ os: [linux]
+
'@swc/core-linux-arm64-gnu@1.13.5':
resolution: {integrity: sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
+ '@swc/core-linux-arm64-gnu@1.15.3':
+ resolution: {integrity: sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+
'@swc/core-linux-arm64-musl@1.13.5':
resolution: {integrity: sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
+ '@swc/core-linux-arm64-musl@1.15.3':
+ resolution: {integrity: sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+
'@swc/core-linux-x64-gnu@1.13.5':
resolution: {integrity: sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
+ '@swc/core-linux-x64-gnu@1.15.3':
+ resolution: {integrity: sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+
'@swc/core-linux-x64-musl@1.13.5':
resolution: {integrity: sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
+ '@swc/core-linux-x64-musl@1.15.3':
+ resolution: {integrity: sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+
'@swc/core-win32-arm64-msvc@1.13.5':
resolution: {integrity: sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==}
engines: {node: '>=10'}
cpu: [arm64]
os: [win32]
+ '@swc/core-win32-arm64-msvc@1.15.3':
+ resolution: {integrity: sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [win32]
+
'@swc/core-win32-ia32-msvc@1.13.5':
resolution: {integrity: sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==}
engines: {node: '>=10'}
cpu: [ia32]
os: [win32]
+ '@swc/core-win32-ia32-msvc@1.15.3':
+ resolution: {integrity: sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw==}
+ engines: {node: '>=10'}
+ cpu: [ia32]
+ os: [win32]
+
'@swc/core-win32-x64-msvc@1.13.5':
resolution: {integrity: sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==}
engines: {node: '>=10'}
cpu: [x64]
os: [win32]
+ '@swc/core-win32-x64-msvc@1.15.3':
+ resolution: {integrity: sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [win32]
+
'@swc/core@1.13.5':
resolution: {integrity: sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==}
engines: {node: '>=10'}
@@ -5155,6 +5233,15 @@ packages:
'@swc/helpers':
optional: true
+ '@swc/core@1.15.3':
+ resolution: {integrity: sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@swc/helpers': '>=0.5.17'
+ peerDependenciesMeta:
+ '@swc/helpers':
+ optional: true
+
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
@@ -5684,6 +5771,14 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
+ '@typescript-eslint/eslint-plugin@8.48.1':
+ resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.35.1
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
'@typescript-eslint/parser@8.43.0':
resolution: {integrity: sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -5697,6 +5792,12 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
+ '@typescript-eslint/project-service@8.48.1':
+ resolution: {integrity: sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
'@typescript-eslint/scope-manager@5.62.0':
resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -5713,12 +5814,22 @@ packages:
resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@typescript-eslint/scope-manager@8.48.1':
+ resolution: {integrity: sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
'@typescript-eslint/tsconfig-utils@8.43.0':
resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
+ '@typescript-eslint/tsconfig-utils@8.48.1':
+ resolution: {integrity: sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
'@typescript-eslint/type-utils@5.62.0':
resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -5746,6 +5857,13 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
+ '@typescript-eslint/type-utils@8.48.1':
+ resolution: {integrity: sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
'@typescript-eslint/types@5.62.0':
resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -5762,6 +5880,10 @@ packages:
resolution: {integrity: sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@typescript-eslint/types@8.48.1':
+ resolution: {integrity: sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
'@typescript-eslint/typescript-estree@5.62.0':
resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -5795,6 +5917,12 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
+ '@typescript-eslint/typescript-estree@8.48.1':
+ resolution: {integrity: sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
'@typescript-eslint/utils@5.62.0':
resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -5820,6 +5948,13 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
+ '@typescript-eslint/utils@8.48.1':
+ resolution: {integrity: sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
'@typescript-eslint/visitor-keys@5.62.0':
resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -5836,6 +5971,10 @@ packages:
resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@typescript-eslint/visitor-keys@8.48.1':
+ resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -6859,17 +6998,10 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
- color-string@1.9.1:
- resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
-
color-support@1.1.3:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
hasBin: true
- color@4.2.3:
- resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
- engines: {node: '>=12.5.0'}
-
colord@2.9.3:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
@@ -7571,6 +7703,10 @@ packages:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'}
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
@@ -7938,8 +8074,8 @@ packages:
engines: {node: '>=6.0'}
hasBin: true
- eslint-config-next@15.4.0-canary.86:
- resolution: {integrity: sha512-nMQzamY2GWhvScnfkfOVeq38tCt/TfyJyHMIzVYarpfyRj286Jk8ZkpgzQT8JtyeQ39kxTDZNBrB4CrWODYg4g==}
+ eslint-config-next@15.5.7:
+ resolution: {integrity: sha512-nU/TRGHHeG81NeLW5DeQT5t6BDUqbpsNQTvef1ld/tqHT+/zTx60/TIhKnmPISTTe++DVo+DLxDmk4rnwHaZVw==}
peerDependencies:
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
typescript: '>=3.3.1'
@@ -9085,9 +9221,6 @@ packages:
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
- is-arrayish@0.3.2:
- resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
-
is-async-function@2.1.1:
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
engines: {node: '>= 0.4'}
@@ -10640,8 +10773,8 @@ packages:
react: '*'
react-dom: '*'
- next@15.4.0-canary.86:
- resolution: {integrity: sha512-lGeO0sOvPZ7oFIklqRA863YzRL1bW+kT/OqU3N6RBquHldiucZwnZKQceZdn6WcHEFmWIHzZV+SMG1JEK7hZLg==}
+ next@15.5.7:
+ resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@@ -12628,6 +12761,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ semver@7.7.3:
+ resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
+ engines: {node: '>=10'}
+ hasBin: true
+
send@0.19.0:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'}
@@ -12682,8 +12820,8 @@ packages:
shallowequal@1.1.0:
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
- sharp@0.34.3:
- resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==}
+ sharp@0.34.5:
+ resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@2.0.0:
@@ -12751,9 +12889,6 @@ packages:
resolution: {integrity: sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==}
engines: {node: ^18.17.0 || >=20.5.0}
- simple-swizzle@0.2.2:
- resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
-
simple-update-notifier@2.0.0:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'}
@@ -14569,13 +14704,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@angular-eslint/eslint-plugin-template@19.8.1(@angular-eslint/template-parser@19.8.1(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.43.0)(@typescript-eslint/utils@8.43.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)':
+ '@angular-eslint/eslint-plugin-template@19.8.1(@angular-eslint/template-parser@19.8.1(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.48.1)(@typescript-eslint/utils@8.48.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)':
dependencies:
'@angular-eslint/bundled-angular-compiler': 19.8.1
'@angular-eslint/template-parser': 19.8.1(eslint@8.57.1)(typescript@5.9.2)
- '@angular-eslint/utils': 19.8.1(@typescript-eslint/utils@8.43.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
- '@typescript-eslint/types': 8.43.0
- '@typescript-eslint/utils': 8.43.0(eslint@8.57.1)(typescript@5.9.2)
+ '@angular-eslint/utils': 19.8.1(@typescript-eslint/utils@8.48.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
+ '@typescript-eslint/types': 8.48.1
+ '@typescript-eslint/utils': 8.48.1(eslint@8.57.1)(typescript@5.9.2)
aria-query: 5.3.2
axobject-query: 4.1.0
eslint: 8.57.1
@@ -14591,20 +14726,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@angular-eslint/eslint-plugin@19.8.1(@typescript-eslint/utils@8.43.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)':
+ '@angular-eslint/eslint-plugin@19.8.1(@typescript-eslint/utils@8.48.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)':
dependencies:
'@angular-eslint/bundled-angular-compiler': 19.8.1
- '@angular-eslint/utils': 19.8.1(@typescript-eslint/utils@8.43.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
- '@typescript-eslint/utils': 8.43.0(eslint@8.57.1)(typescript@5.9.2)
+ '@angular-eslint/utils': 19.8.1(@typescript-eslint/utils@8.48.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
+ '@typescript-eslint/utils': 8.48.1(eslint@8.57.1)(typescript@5.9.2)
eslint: 8.57.1
typescript: 5.9.2
- '@angular-eslint/schematics@19.8.1(@angular-eslint/template-parser@19.8.1(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.43.0)(@typescript-eslint/utils@8.43.0(eslint@8.57.1)(typescript@5.9.2))(chokidar@4.0.3)(eslint@8.57.1)(typescript@5.9.2)':
+ '@angular-eslint/schematics@19.8.1(@angular-eslint/template-parser@19.8.1(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.48.1)(@typescript-eslint/utils@8.48.1(eslint@8.57.1)(typescript@5.9.2))(chokidar@4.0.3)(eslint@8.57.1)(typescript@5.9.2)':
dependencies:
'@angular-devkit/core': 19.2.17(chokidar@4.0.3)
'@angular-devkit/schematics': 19.2.17(chokidar@4.0.3)
- '@angular-eslint/eslint-plugin': 19.8.1(@typescript-eslint/utils@8.43.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
- '@angular-eslint/eslint-plugin-template': 19.8.1(@angular-eslint/template-parser@19.8.1(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.43.0)(@typescript-eslint/utils@8.43.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
+ '@angular-eslint/eslint-plugin': 19.8.1(@typescript-eslint/utils@8.48.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
+ '@angular-eslint/eslint-plugin-template': 19.8.1(@angular-eslint/template-parser@19.8.1(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.48.1)(@typescript-eslint/utils@8.48.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
ignore: 7.0.5
semver: 7.7.2
strip-json-comments: 3.1.1
@@ -14632,10 +14767,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@angular-eslint/utils@19.8.1(@typescript-eslint/utils@8.43.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)':
+ '@angular-eslint/utils@19.8.1(@typescript-eslint/utils@8.48.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)':
dependencies:
'@angular-eslint/bundled-angular-compiler': 19.8.1
- '@typescript-eslint/utils': 8.43.0(eslint@8.57.1)(typescript@5.9.2)
+ '@typescript-eslint/utils': 8.48.1(eslint@8.57.1)(typescript@5.9.2)
eslint: 8.57.1
typescript: 5.9.2
@@ -17254,6 +17389,11 @@ snapshots:
dependencies:
tslib: 2.8.1
+ '@emnapi/runtime@1.7.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
'@emnapi/wasi-threads@1.1.0':
dependencies:
tslib: 2.8.1
@@ -17611,90 +17751,101 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@img/sharp-darwin-arm64@0.34.3':
+ '@img/colour@1.0.0':
+ optional: true
+
+ '@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-darwin-arm64': 1.2.0
+ '@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
- '@img/sharp-darwin-x64@0.34.3':
+ '@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-darwin-x64': 1.2.0
+ '@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
- '@img/sharp-libvips-darwin-arm64@1.2.0':
+ '@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
- '@img/sharp-libvips-darwin-x64@1.2.0':
+ '@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
- '@img/sharp-libvips-linux-arm64@1.2.0':
+ '@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
- '@img/sharp-libvips-linux-arm@1.2.0':
+ '@img/sharp-libvips-linux-arm@1.2.4':
optional: true
- '@img/sharp-libvips-linux-ppc64@1.2.0':
+ '@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true
- '@img/sharp-libvips-linux-s390x@1.2.0':
+ '@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
- '@img/sharp-libvips-linux-x64@1.2.0':
+ '@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
- '@img/sharp-libvips-linuxmusl-arm64@1.2.0':
+ '@img/sharp-libvips-linux-x64@1.2.4':
optional: true
- '@img/sharp-libvips-linuxmusl-x64@1.2.0':
+ '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
- '@img/sharp-linux-arm64@0.34.3':
+ '@img/sharp-libvips-linuxmusl-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linux-arm64': 1.2.0
+ '@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
- '@img/sharp-linux-arm@0.34.3':
+ '@img/sharp-linux-arm@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linux-arm': 1.2.0
+ '@img/sharp-libvips-linux-arm': 1.2.4
optional: true
- '@img/sharp-linux-ppc64@0.34.3':
+ '@img/sharp-linux-ppc64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linux-ppc64': 1.2.0
+ '@img/sharp-libvips-linux-ppc64': 1.2.4
optional: true
- '@img/sharp-linux-s390x@0.34.3':
+ '@img/sharp-linux-riscv64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linux-s390x': 1.2.0
+ '@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
- '@img/sharp-linux-x64@0.34.3':
+ '@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linux-x64': 1.2.0
+ '@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
- '@img/sharp-linuxmusl-arm64@0.34.3':
+ '@img/sharp-linux-x64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linuxmusl-arm64': 1.2.0
+ '@img/sharp-libvips-linux-x64': 1.2.4
optional: true
- '@img/sharp-linuxmusl-x64@0.34.3':
+ '@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
- '@img/sharp-libvips-linuxmusl-x64': 1.2.0
+ '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
- '@img/sharp-wasm32@0.34.3':
+ '@img/sharp-linuxmusl-x64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-wasm32@0.34.5':
dependencies:
- '@emnapi/runtime': 1.5.0
+ '@emnapi/runtime': 1.7.1
optional: true
- '@img/sharp-win32-arm64@0.34.3':
+ '@img/sharp-win32-arm64@0.34.5':
optional: true
- '@img/sharp-win32-ia32@0.34.3':
+ '@img/sharp-win32-ia32@0.34.5':
optional: true
- '@img/sharp-win32-x64@0.34.3':
+ '@img/sharp-win32-x64@0.34.5':
optional: true
'@inkeep/cxkit-color-mode@0.5.97': {}
@@ -18256,7 +18407,7 @@ snapshots:
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
'@emnapi/core': 1.5.0
- '@emnapi/runtime': 1.5.0
+ '@emnapi/runtime': 1.7.1
'@tybys/wasm-util': 0.10.0
optional: true
@@ -18269,7 +18420,7 @@ snapshots:
'@napi-rs/wasm-runtime@1.0.4':
dependencies:
'@emnapi/core': 1.5.0
- '@emnapi/runtime': 1.5.0
+ '@emnapi/runtime': 1.7.1
'@tybys/wasm-util': 0.10.0
optional: true
@@ -18285,34 +18436,34 @@ snapshots:
yaml: 2.8.1
yargs: 17.7.2
- '@next/env@15.4.0-canary.86': {}
+ '@next/env@15.5.7': {}
- '@next/eslint-plugin-next@15.4.0-canary.86':
+ '@next/eslint-plugin-next@15.5.7':
dependencies:
fast-glob: 3.3.1
- '@next/swc-darwin-arm64@15.4.0-canary.86':
+ '@next/swc-darwin-arm64@15.5.7':
optional: true
- '@next/swc-darwin-x64@15.4.0-canary.86':
+ '@next/swc-darwin-x64@15.5.7':
optional: true
- '@next/swc-linux-arm64-gnu@15.4.0-canary.86':
+ '@next/swc-linux-arm64-gnu@15.5.7':
optional: true
- '@next/swc-linux-arm64-musl@15.4.0-canary.86':
+ '@next/swc-linux-arm64-musl@15.5.7':
optional: true
- '@next/swc-linux-x64-gnu@15.4.0-canary.86':
+ '@next/swc-linux-x64-gnu@15.5.7':
optional: true
- '@next/swc-linux-x64-musl@15.4.0-canary.86':
+ '@next/swc-linux-x64-musl@15.5.7':
optional: true
- '@next/swc-win32-arm64-msvc@15.4.0-canary.86':
+ '@next/swc-win32-arm64-msvc@15.5.7':
optional: true
- '@next/swc-win32-x64-msvc@15.4.0-canary.86':
+ '@next/swc-win32-x64-msvc@15.5.7':
optional: true
'@ng-icons/core@32.1.0(@angular/common@20.3.3(@angular/core@20.3.3(@angular/compiler@20.3.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.3(@angular/compiler@20.3.3)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)':
@@ -18362,7 +18513,7 @@ snapshots:
'@npmcli/fs@4.0.0':
dependencies:
- semver: 7.7.2
+ semver: 7.7.3
'@npmcli/git@6.0.3':
dependencies:
@@ -18372,7 +18523,7 @@ snapshots:
npm-pick-manifest: 10.0.0
proc-log: 5.0.0
promise-retry: 2.0.1
- semver: 7.7.2
+ semver: 7.7.3
which: 5.0.0
'@npmcli/installed-package-contents@3.0.0':
@@ -18389,7 +18540,7 @@ snapshots:
hosted-git-info: 8.1.0
json-parse-even-better-errors: 4.0.0
proc-log: 5.0.0
- semver: 7.7.2
+ semver: 7.7.3
validate-npm-package-license: 3.0.4
'@npmcli/promise-spawn@8.0.3':
@@ -18622,7 +18773,7 @@ snapshots:
extract-zip: 2.0.1
progress: 2.0.3
proxy-agent: 6.5.0
- semver: 7.7.2
+ semver: 7.7.3
tar-fs: 3.1.0
unbzip2-stream: 1.4.3
yargs: 17.7.2
@@ -19641,7 +19792,7 @@ snapshots:
'@rtsao/scc@1.1.0': {}
- '@rushstack/eslint-patch@1.12.0': {}
+ '@rushstack/eslint-patch@1.15.0': {}
'@schematics/angular@20.3.4(chokidar@4.0.3)':
dependencies:
@@ -19833,33 +19984,63 @@ snapshots:
'@swc/core-darwin-arm64@1.13.5':
optional: true
+ '@swc/core-darwin-arm64@1.15.3':
+ optional: true
+
'@swc/core-darwin-x64@1.13.5':
optional: true
+ '@swc/core-darwin-x64@1.15.3':
+ optional: true
+
'@swc/core-linux-arm-gnueabihf@1.13.5':
optional: true
+ '@swc/core-linux-arm-gnueabihf@1.15.3':
+ optional: true
+
'@swc/core-linux-arm64-gnu@1.13.5':
optional: true
+ '@swc/core-linux-arm64-gnu@1.15.3':
+ optional: true
+
'@swc/core-linux-arm64-musl@1.13.5':
optional: true
+ '@swc/core-linux-arm64-musl@1.15.3':
+ optional: true
+
'@swc/core-linux-x64-gnu@1.13.5':
optional: true
+ '@swc/core-linux-x64-gnu@1.15.3':
+ optional: true
+
'@swc/core-linux-x64-musl@1.13.5':
optional: true
+ '@swc/core-linux-x64-musl@1.15.3':
+ optional: true
+
'@swc/core-win32-arm64-msvc@1.13.5':
optional: true
+ '@swc/core-win32-arm64-msvc@1.15.3':
+ optional: true
+
'@swc/core-win32-ia32-msvc@1.13.5':
optional: true
+ '@swc/core-win32-ia32-msvc@1.15.3':
+ optional: true
+
'@swc/core-win32-x64-msvc@1.13.5':
optional: true
+ '@swc/core-win32-x64-msvc@1.15.3':
+ optional: true
+
'@swc/core@1.13.5(@swc/helpers@0.5.17)':
dependencies:
'@swc/counter': 0.1.3
@@ -19877,6 +20058,24 @@ snapshots:
'@swc/core-win32-x64-msvc': 1.13.5
'@swc/helpers': 0.5.17
+ '@swc/core@1.15.3(@swc/helpers@0.5.17)':
+ dependencies:
+ '@swc/counter': 0.1.3
+ '@swc/types': 0.1.25
+ optionalDependencies:
+ '@swc/core-darwin-arm64': 1.15.3
+ '@swc/core-darwin-x64': 1.15.3
+ '@swc/core-linux-arm-gnueabihf': 1.15.3
+ '@swc/core-linux-arm64-gnu': 1.15.3
+ '@swc/core-linux-arm64-musl': 1.15.3
+ '@swc/core-linux-x64-gnu': 1.15.3
+ '@swc/core-linux-x64-musl': 1.15.3
+ '@swc/core-win32-arm64-msvc': 1.15.3
+ '@swc/core-win32-ia32-msvc': 1.15.3
+ '@swc/core-win32-x64-msvc': 1.15.3
+ '@swc/helpers': 0.5.17
+ optional: true
+
'@swc/counter@0.1.3': {}
'@swc/helpers@0.5.15':
@@ -20491,6 +20690,23 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.1
+ '@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.2)
+ '@typescript-eslint/scope-manager': 8.48.1
+ '@typescript-eslint/type-utils': 8.48.1(eslint@8.57.1)(typescript@5.9.2)
+ '@typescript-eslint/utils': 8.48.1(eslint@8.57.1)(typescript@5.9.2)
+ '@typescript-eslint/visitor-keys': 8.48.1
+ eslint: 8.57.1
+ graphemer: 1.4.0
+ ignore: 7.0.5
+ natural-compare: 1.4.0
+ ts-api-utils: 2.1.0(typescript@5.9.2)
+ typescript: 5.9.2
+ transitivePeerDependencies:
+ - supports-color
+
'@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.2)':
dependencies:
'@typescript-eslint/scope-manager': 8.43.0
@@ -20512,6 +20728,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@typescript-eslint/project-service@8.48.1(typescript@5.9.2)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.2)
+ '@typescript-eslint/types': 8.48.1
+ debug: 4.4.1(supports-color@5.5.0)
+ typescript: 5.9.2
+ transitivePeerDependencies:
+ - supports-color
+
'@typescript-eslint/scope-manager@5.62.0':
dependencies:
'@typescript-eslint/types': 5.62.0
@@ -20532,10 +20757,19 @@ snapshots:
'@typescript-eslint/types': 8.43.0
'@typescript-eslint/visitor-keys': 8.43.0
+ '@typescript-eslint/scope-manager@8.48.1':
+ dependencies:
+ '@typescript-eslint/types': 8.48.1
+ '@typescript-eslint/visitor-keys': 8.48.1
+
'@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.9.2)':
dependencies:
typescript: 5.9.2
+ '@typescript-eslint/tsconfig-utils@8.48.1(typescript@5.9.2)':
+ dependencies:
+ typescript: 5.9.2
+
'@typescript-eslint/type-utils@5.62.0(eslint@8.57.1)(typescript@5.9.2)':
dependencies:
'@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.2)
@@ -20572,6 +20806,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@typescript-eslint/type-utils@8.48.1(eslint@8.57.1)(typescript@5.9.2)':
+ dependencies:
+ '@typescript-eslint/types': 8.48.1
+ '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.2)
+ '@typescript-eslint/utils': 8.48.1(eslint@8.57.1)(typescript@5.9.2)
+ debug: 4.4.1(supports-color@5.5.0)
+ eslint: 8.57.1
+ ts-api-utils: 2.1.0(typescript@5.9.2)
+ typescript: 5.9.2
+ transitivePeerDependencies:
+ - supports-color
+
'@typescript-eslint/types@5.62.0': {}
'@typescript-eslint/types@7.18.0': {}
@@ -20580,6 +20826,8 @@ snapshots:
'@typescript-eslint/types@8.43.0': {}
+ '@typescript-eslint/types@8.48.1': {}
+
'@typescript-eslint/typescript-estree@5.62.0(typescript@5.9.2)':
dependencies:
'@typescript-eslint/types': 5.62.0
@@ -20587,7 +20835,7 @@ snapshots:
debug: 4.4.1(supports-color@5.5.0)
globby: 11.1.0
is-glob: 4.0.3
- semver: 7.7.2
+ semver: 7.7.3
tsutils: 3.21.0(typescript@5.9.2)
optionalDependencies:
typescript: 5.9.2
@@ -20617,7 +20865,7 @@ snapshots:
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.5
- semver: 7.7.2
+ semver: 7.7.3
ts-api-utils: 1.4.3(typescript@5.9.2)
optionalDependencies:
typescript: 5.9.2
@@ -20640,6 +20888,21 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@typescript-eslint/typescript-estree@8.48.1(typescript@5.9.2)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.48.1(typescript@5.9.2)
+ '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.2)
+ '@typescript-eslint/types': 8.48.1
+ '@typescript-eslint/visitor-keys': 8.48.1
+ debug: 4.4.1(supports-color@5.5.0)
+ minimatch: 9.0.5
+ semver: 7.7.3
+ tinyglobby: 0.2.15
+ ts-api-utils: 2.1.0(typescript@5.9.2)
+ typescript: 5.9.2
+ transitivePeerDependencies:
+ - supports-color
+
'@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.9.2)':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
@@ -20688,6 +20951,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@typescript-eslint/utils@8.48.1(eslint@8.57.1)(typescript@5.9.2)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
+ '@typescript-eslint/scope-manager': 8.48.1
+ '@typescript-eslint/types': 8.48.1
+ '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.2)
+ eslint: 8.57.1
+ typescript: 5.9.2
+ transitivePeerDependencies:
+ - supports-color
+
'@typescript-eslint/visitor-keys@5.62.0':
dependencies:
'@typescript-eslint/types': 5.62.0
@@ -20708,6 +20982,11 @@ snapshots:
'@typescript-eslint/types': 8.43.0
eslint-visitor-keys: 4.2.1
+ '@typescript-eslint/visitor-keys@8.48.1':
+ dependencies:
+ '@typescript-eslint/types': 8.48.1
+ eslint-visitor-keys: 4.2.1
+
'@ungap/structured-clone@1.3.0': {}
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
@@ -21858,20 +22137,8 @@ snapshots:
color-name@1.1.4: {}
- color-string@1.9.1:
- dependencies:
- color-name: 1.1.4
- simple-swizzle: 0.2.2
- optional: true
-
color-support@1.1.3: {}
- color@4.2.3:
- dependencies:
- color-convert: 2.0.1
- color-string: 1.9.1
- optional: true
-
colord@2.9.3: {}
colorette@1.4.0: {}
@@ -22112,7 +22379,7 @@ snapshots:
postcss-modules-scope: 3.2.1(postcss@8.5.3)
postcss-modules-values: 4.0.0(postcss@8.5.3)
postcss-value-parser: 4.2.0
- semver: 7.7.2
+ semver: 7.7.3
optionalDependencies:
'@rspack/core': 1.5.3(@swc/helpers@0.5.17)
webpack: 5.95.0(@swc/core@1.13.5(@swc/helpers@0.5.17))
@@ -22667,6 +22934,9 @@ snapshots:
detect-libc@2.0.4: {}
+ detect-libc@2.1.2:
+ optional: true
+
detect-node-es@1.1.0: {}
detect-node@2.1.0: {}
@@ -23217,11 +23487,11 @@ snapshots:
source-map: 0.6.1
optional: true
- eslint-config-next@15.4.0-canary.86(eslint@8.57.1)(typescript@5.9.2):
+ eslint-config-next@15.5.7(eslint@8.57.1)(typescript@5.9.2):
dependencies:
- '@next/eslint-plugin-next': 15.4.0-canary.86
- '@rushstack/eslint-patch': 1.12.0
- '@typescript-eslint/eslint-plugin': 8.43.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
+ '@next/eslint-plugin-next': 15.5.7
+ '@rushstack/eslint-patch': 1.15.0
+ '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
'@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.2)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
@@ -24796,9 +25066,6 @@ snapshots:
is-arrayish@0.2.1: {}
- is-arrayish@0.3.2:
- optional: true
-
is-async-function@2.1.1:
dependencies:
async-function: 1.0.0
@@ -24824,7 +25091,7 @@ snapshots:
is-bun-module@2.0.0:
dependencies:
- semver: 7.7.2
+ semver: 7.7.3
is-callable@1.2.7: {}
@@ -25722,7 +25989,7 @@ snapshots:
make-dir@4.0.0:
dependencies:
- semver: 7.7.2
+ semver: 7.7.3
make-dir@5.1.0: {}
@@ -26988,23 +27255,23 @@ snapshots:
netmask@2.0.2:
optional: true
- next-intl@3.26.5(next@15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react@19.1.0):
+ next-intl@3.26.5(next@15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react@19.1.0):
dependencies:
'@formatjs/intl-localematcher': 0.5.10
negotiator: 1.0.0
- next: 15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1)
+ next: 15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1)
react: 19.1.0
use-intl: 3.26.5(react@19.1.0)
- next-themes@0.2.1(next@15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+ next-themes@0.2.1(next@15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
- next: 15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1)
+ next: 15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
- next@15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1):
+ next@15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1):
dependencies:
- '@next/env': 15.4.0-canary.86
+ '@next/env': 15.5.7
'@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001741
postcss: 8.4.31
@@ -27012,17 +27279,17 @@ snapshots:
react-dom: 19.1.0(react@19.1.0)
styled-jsx: 5.1.6(@babel/core@7.28.4)(react@19.1.0)
optionalDependencies:
- '@next/swc-darwin-arm64': 15.4.0-canary.86
- '@next/swc-darwin-x64': 15.4.0-canary.86
- '@next/swc-linux-arm64-gnu': 15.4.0-canary.86
- '@next/swc-linux-arm64-musl': 15.4.0-canary.86
- '@next/swc-linux-x64-gnu': 15.4.0-canary.86
- '@next/swc-linux-x64-musl': 15.4.0-canary.86
- '@next/swc-win32-arm64-msvc': 15.4.0-canary.86
- '@next/swc-win32-x64-msvc': 15.4.0-canary.86
+ '@next/swc-darwin-arm64': 15.5.7
+ '@next/swc-darwin-x64': 15.5.7
+ '@next/swc-linux-arm64-gnu': 15.5.7
+ '@next/swc-linux-arm64-musl': 15.5.7
+ '@next/swc-linux-x64-gnu': 15.5.7
+ '@next/swc-linux-x64-musl': 15.5.7
+ '@next/swc-win32-arm64-msvc': 15.5.7
+ '@next/swc-win32-x64-msvc': 15.5.7
'@playwright/test': 1.55.0
sass: 1.92.1
- sharp: 0.34.3
+ sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@@ -27085,7 +27352,7 @@ snapshots:
node-gyp-build-optional-packages@5.2.2:
dependencies:
- detect-libc: 2.0.4
+ detect-libc: 2.1.2
optional: true
node-gyp@11.4.2:
@@ -27096,7 +27363,7 @@ snapshots:
make-fetch-happen: 14.0.3
nopt: 8.1.0
proc-log: 5.0.0
- semver: 7.7.2
+ semver: 7.7.3
tar: 7.5.1
tinyglobby: 0.2.15
which: 5.0.0
@@ -27135,7 +27402,7 @@ snapshots:
normalize-package-data@6.0.2:
dependencies:
hosted-git-info: 7.0.2
- semver: 7.7.2
+ semver: 7.7.3
validate-npm-package-license: 3.0.4
normalize-path@3.0.0: {}
@@ -27150,7 +27417,7 @@ snapshots:
npm-install-checks@7.1.2:
dependencies:
- semver: 7.7.2
+ semver: 7.7.3
npm-normalize-package-bin@4.0.0: {}
@@ -27158,7 +27425,7 @@ snapshots:
dependencies:
hosted-git-info: 8.1.0
proc-log: 5.0.0
- semver: 7.7.2
+ semver: 7.7.3
validate-npm-package-name: 6.0.2
npm-package-arg@13.0.0:
@@ -27178,7 +27445,7 @@ snapshots:
npm-install-checks: 7.1.2
npm-normalize-package-bin: 4.0.0
npm-package-arg: 12.0.2
- semver: 7.7.2
+ semver: 7.7.3
npm-registry-fetch@18.0.2:
dependencies:
@@ -27222,7 +27489,7 @@ snapshots:
nwsapi@2.2.22: {}
- nx@21.6.1(@swc/core@1.13.5(@swc/helpers@0.5.17)):
+ nx@21.6.1(@swc/core@1.15.3(@swc/helpers@0.5.17)):
dependencies:
'@napi-rs/wasm-runtime': 0.2.4
'@yarnpkg/lockfile': 1.1.0
@@ -27270,7 +27537,7 @@ snapshots:
'@nx/nx-linux-x64-musl': 21.6.3
'@nx/nx-win32-arm64-msvc': 21.6.3
'@nx/nx-win32-x64-msvc': 21.6.3
- '@swc/core': 1.13.5(@swc/helpers@0.5.17)
+ '@swc/core': 1.15.3(@swc/helpers@0.5.17)
transitivePeerDependencies:
- debug
@@ -27599,7 +27866,7 @@ snapshots:
got: 12.6.1
registry-auth-token: 5.1.0
registry-url: 6.0.1
- semver: 7.7.2
+ semver: 7.7.3
package-manager-detector@0.2.11:
dependencies:
@@ -28034,7 +28301,7 @@ snapshots:
cosmiconfig: 8.3.6(typescript@5.9.2)
jiti: 1.21.7
postcss: 8.5.6
- semver: 7.7.2
+ semver: 7.7.3
webpack: 5.95.0(@swc/core@1.13.5(@swc/helpers@0.5.17))
transitivePeerDependencies:
- typescript
@@ -29505,6 +29772,8 @@ snapshots:
semver@7.7.2: {}
+ semver@7.7.3: {}
+
send@0.19.0:
dependencies:
debug: 2.6.9
@@ -29617,34 +29886,36 @@ snapshots:
shallowequal@1.1.0: {}
- sharp@0.34.3:
+ sharp@0.34.5:
dependencies:
- color: 4.2.3
- detect-libc: 2.0.4
- semver: 7.7.2
+ '@img/colour': 1.0.0
+ detect-libc: 2.1.2
+ semver: 7.7.3
optionalDependencies:
- '@img/sharp-darwin-arm64': 0.34.3
- '@img/sharp-darwin-x64': 0.34.3
- '@img/sharp-libvips-darwin-arm64': 1.2.0
- '@img/sharp-libvips-darwin-x64': 1.2.0
- '@img/sharp-libvips-linux-arm': 1.2.0
- '@img/sharp-libvips-linux-arm64': 1.2.0
- '@img/sharp-libvips-linux-ppc64': 1.2.0
- '@img/sharp-libvips-linux-s390x': 1.2.0
- '@img/sharp-libvips-linux-x64': 1.2.0
- '@img/sharp-libvips-linuxmusl-arm64': 1.2.0
- '@img/sharp-libvips-linuxmusl-x64': 1.2.0
- '@img/sharp-linux-arm': 0.34.3
- '@img/sharp-linux-arm64': 0.34.3
- '@img/sharp-linux-ppc64': 0.34.3
- '@img/sharp-linux-s390x': 0.34.3
- '@img/sharp-linux-x64': 0.34.3
- '@img/sharp-linuxmusl-arm64': 0.34.3
- '@img/sharp-linuxmusl-x64': 0.34.3
- '@img/sharp-wasm32': 0.34.3
- '@img/sharp-win32-arm64': 0.34.3
- '@img/sharp-win32-ia32': 0.34.3
- '@img/sharp-win32-x64': 0.34.3
+ '@img/sharp-darwin-arm64': 0.34.5
+ '@img/sharp-darwin-x64': 0.34.5
+ '@img/sharp-libvips-darwin-arm64': 1.2.4
+ '@img/sharp-libvips-darwin-x64': 1.2.4
+ '@img/sharp-libvips-linux-arm': 1.2.4
+ '@img/sharp-libvips-linux-arm64': 1.2.4
+ '@img/sharp-libvips-linux-ppc64': 1.2.4
+ '@img/sharp-libvips-linux-riscv64': 1.2.4
+ '@img/sharp-libvips-linux-s390x': 1.2.4
+ '@img/sharp-libvips-linux-x64': 1.2.4
+ '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
+ '@img/sharp-libvips-linuxmusl-x64': 1.2.4
+ '@img/sharp-linux-arm': 0.34.5
+ '@img/sharp-linux-arm64': 0.34.5
+ '@img/sharp-linux-ppc64': 0.34.5
+ '@img/sharp-linux-riscv64': 0.34.5
+ '@img/sharp-linux-s390x': 0.34.5
+ '@img/sharp-linux-x64': 0.34.5
+ '@img/sharp-linuxmusl-arm64': 0.34.5
+ '@img/sharp-linuxmusl-x64': 0.34.5
+ '@img/sharp-wasm32': 0.34.5
+ '@img/sharp-win32-arm64': 0.34.5
+ '@img/sharp-win32-ia32': 0.34.5
+ '@img/sharp-win32-x64': 0.34.5
optional: true
shebang-command@2.0.0:
@@ -29732,11 +30003,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- simple-swizzle@0.2.2:
- dependencies:
- is-arrayish: 0.3.2
- optional: true
-
simple-update-notifier@2.0.0:
dependencies:
semver: 7.7.2
@@ -30453,7 +30719,7 @@ snapshots:
tslib@2.8.1: {}
- tsup@8.5.0(@swc/core@1.13.5(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1):
+ tsup@8.5.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(jiti@2.5.1)(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1):
dependencies:
bundle-require: 5.1.0(esbuild@0.25.9)
cac: 6.7.14
@@ -30473,7 +30739,7 @@ snapshots:
tinyglobby: 0.2.15
tree-kill: 1.2.2
optionalDependencies:
- '@swc/core': 1.13.5(@swc/helpers@0.5.17)
+ '@swc/core': 1.15.3(@swc/helpers@0.5.17)
postcss: 8.5.6
typescript: 5.9.2
transitivePeerDependencies: