@zitadel/sdk-next 0.1.0-alpha.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +207 -37
  2. package/dist/auth.d.ts +66 -13
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +162 -7
  5. package/dist/client.d.ts +27 -2
  6. package/dist/client.d.ts.map +1 -0
  7. package/dist/client.js +29 -9
  8. package/dist/components.d.ts +16 -0
  9. package/dist/components.d.ts.map +1 -0
  10. package/dist/components.js +15 -0
  11. package/dist/context.d.ts +21 -10
  12. package/dist/context.d.ts.map +1 -0
  13. package/dist/context.js +24 -8
  14. package/dist/index.d.ts +16 -9
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +13 -24
  17. package/dist/jsx.d.ts +5 -0
  18. package/dist/lib/jwt.d.ts +9 -0
  19. package/dist/lib/jwt.d.ts.map +1 -0
  20. package/dist/lib/jwt.js +7 -0
  21. package/dist/middleware.d.ts +19 -11
  22. package/dist/middleware.d.ts.map +1 -0
  23. package/dist/middleware.js +304 -9
  24. package/dist/provider.d.ts +52 -0
  25. package/dist/provider.d.ts.map +1 -0
  26. package/dist/provider.js +75 -0
  27. package/dist/react.d.ts +26 -0
  28. package/dist/react.d.ts.map +1 -0
  29. package/dist/react.js +24 -0
  30. package/dist/server.d.ts +13 -2
  31. package/dist/server.d.ts.map +1 -0
  32. package/dist/server.js +10 -7
  33. package/dist/session.d.ts +77 -0
  34. package/dist/session.d.ts.map +1 -0
  35. package/dist/session.js +156 -0
  36. package/dist/types.d.ts +8 -3
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +1 -1
  39. package/dist/useAuth.d.ts +17 -5
  40. package/dist/useAuth.d.ts.map +1 -0
  41. package/dist/useAuth.js +18 -7
  42. package/package.json +46 -31
  43. package/dist/chunk-2BFQLJQE.js +0 -27
  44. package/dist/chunk-4KENHIG4.js +0 -222
  45. package/dist/chunk-6F4PWJZI.js +0 -0
  46. package/dist/chunk-B7S6XMT3.js +0 -41
  47. package/dist/chunk-OCZMYSFX.js +0 -13
  48. package/dist/chunk-XTCHTAIQ.js +0 -12
package/dist/client.js CHANGED
@@ -1,9 +1,29 @@
1
- // src/client.ts
2
- import { ZitadelLogin, ZitadelLogout } from "@zitadel/components";
3
- import { configureZitadel, getApi } from "@zitadel/api/config";
4
- export {
5
- ZitadelLogin,
6
- ZitadelLogout,
7
- configureZitadel,
8
- getApi
9
- };
1
+ /**
2
+ * Registers the `<zitadel-login>`, `<zitadel-logout>` and `<zitadel-session>`
3
+ * Lit web components for use in Next.js apps.
4
+ *
5
+ * Import this inside a `"use client"` boundary (e.g. a dynamic import
6
+ * with `{ ssr: false }`) to register the custom elements with the
7
+ * browser's global registry:
8
+ *
9
+ * ```ts
10
+ * await import("@zitadel/sdk-next/client");
11
+ * ```
12
+ *
13
+ * On an edge runtime the components package resolves to an entry without the
14
+ * elements, so this entry still loads there.
15
+ *
16
+ * SDK configuration is done via `configureZitadel()`, re-exported here so
17
+ * a consuming app that only declares `@zitadel/sdk-next` as a direct
18
+ * dependency can configure the SDK without reaching into
19
+ * `@zitadel/api/config` (which strict package managers would not
20
+ * resolve). Call it inside the same `"use client"` boundary before the
21
+ * components mount.
22
+ */
23
+ // Registers <zitadel-login>, <zitadel-logout> and <zitadel-session> in the browser.
24
+ // The edge build of the components package has no elements, so this does nothing there.
25
+ import "@zitadel/components";
26
+ // Re-exported so scaffolded apps can wire the business copy overlay without a
27
+ // direct @zitadel/components dependency (strict package managers reject those).
28
+ export { businessLocales } from "@zitadel/components";
29
+ export { configureZitadel, getApi } from "@zitadel/api/config";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * React components for the Zitadel widgets in Next.js apps.
3
+ *
4
+ * Use these instead of the raw `<zitadel-login>`, `<zitadel-logout>` and
5
+ * `<zitadel-session>` elements: the wrappers set `project` and `locales` as DOM
6
+ * properties on React 18 and 19, including after server-rendered hydration,
7
+ * where the raw elements lose them (React 19) or write `[object Object]`
8
+ * attributes (React 18).
9
+ *
10
+ * ```tsx
11
+ * import { ZitadelLogin } from "@zitadel/sdk-next/components";
12
+ * ```
13
+ */
14
+ export { businessLocales, configureZitadel, ZitadelLogin, ZitadelLogout, ZitadelSession, } from "@zitadel/sdk-react";
15
+ export type { ZitadelLoginReactProps, ZitadelLogoutReactProps, ZitadelSessionReactProps, } from "@zitadel/sdk-react";
16
+ //# sourceMappingURL=components.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,cAAc,GACf,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ /**
3
+ * React components for the Zitadel widgets in Next.js apps.
4
+ *
5
+ * Use these instead of the raw `<zitadel-login>`, `<zitadel-logout>` and
6
+ * `<zitadel-session>` elements: the wrappers set `project` and `locales` as DOM
7
+ * properties on React 18 and 19, including after server-rendered hydration,
8
+ * where the raw elements lose them (React 19) or write `[object Object]`
9
+ * attributes (React 18).
10
+ *
11
+ * ```tsx
12
+ * import { ZitadelLogin } from "@zitadel/sdk-next/components";
13
+ * ```
14
+ */
15
+ export { businessLocales, configureZitadel, ZitadelLogin, ZitadelLogout, ZitadelSession, } from "@zitadel/sdk-react";
package/dist/context.d.ts CHANGED
@@ -1,11 +1,22 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ReactNode } from 'react';
3
- import { AuthResult, NextgenSession } from '@zitadel/sdk-core/types';
4
-
5
- declare function NextgenProvider({ session, children, }: {
6
- session: AuthResult | NextgenSession | null;
1
+ import { type ReactNode } from "react";
2
+ import type { ClientAuthResult } from "./types.js";
3
+ /**
4
+ * Client-side context carrier. In the standard server-seeded setup, render
5
+ * {@link NextgenProvider} from `@zitadel/sdk-next/server` instead — it
6
+ * normalises the server's `auth()` result to the client-safe shape (dropping
7
+ * the raw session token) *before* the value crosses the server→client
8
+ * component boundary, and its `server-only` guard keeps it out of client
9
+ * wrappers where that strip would come too late.
10
+ *
11
+ * Render this component directly only from client code that already holds a
12
+ * client-safe value — e.g. seeding from a `getSession()` read.
13
+ *
14
+ * The `value` prop is deliberately typed as {@link ClientAuthResult}: the
15
+ * raw session token must never enter client-side state.
16
+ */
17
+ export declare function AuthContextProvider({ value, children, }: {
18
+ value: ClientAuthResult;
7
19
  children: ReactNode;
8
- }): react_jsx_runtime.JSX.Element;
9
- declare function useAuthContext(): AuthResult;
10
-
11
- export { NextgenProvider, useAuthContext };
20
+ }): import("react/jsx-runtime").JSX.Element;
21
+ export declare function useAuthContext(): ClientAuthResult;
22
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAElE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAMnD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,gBAAgB,CAAC;IACxB,QAAQ,EAAE,SAAS,CAAC;CACrB,2CAEA;AAED,wBAAgB,cAAc,IAAI,gBAAgB,CAEjD"}
package/dist/context.js CHANGED
@@ -1,9 +1,25 @@
1
1
  "use client";
2
- import {
3
- NextgenProvider,
4
- useAuthContext
5
- } from "./chunk-2BFQLJQE.js";
6
- export {
7
- NextgenProvider,
8
- useAuthContext
9
- };
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from "react";
4
+ const defaultValue = { isAuthenticated: false, session: null };
5
+ const NextgenAuthContext = createContext(defaultValue);
6
+ /**
7
+ * Client-side context carrier. In the standard server-seeded setup, render
8
+ * {@link NextgenProvider} from `@zitadel/sdk-next/server` instead — it
9
+ * normalises the server's `auth()` result to the client-safe shape (dropping
10
+ * the raw session token) *before* the value crosses the server→client
11
+ * component boundary, and its `server-only` guard keeps it out of client
12
+ * wrappers where that strip would come too late.
13
+ *
14
+ * Render this component directly only from client code that already holds a
15
+ * client-safe value — e.g. seeding from a `getSession()` read.
16
+ *
17
+ * The `value` prop is deliberately typed as {@link ClientAuthResult}: the
18
+ * raw session token must never enter client-side state.
19
+ */
20
+ export function AuthContextProvider({ value, children, }) {
21
+ return _jsx(NextgenAuthContext.Provider, { value: value, children: children });
22
+ }
23
+ export function useAuthContext() {
24
+ return useContext(NextgenAuthContext);
25
+ }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,16 @@
1
- export { AuthResult, AuthState, NextgenMiddlewareOptions, NextgenSession, UnauthState } from '@zitadel/sdk-core/types';
2
- export { ProxyHandler, ProxyOptions, createProxy, nextgenMiddleware } from './middleware.js';
3
- export { auth } from './auth.js';
4
- export { NextgenProvider, useAuthContext } from './context.js';
5
- export { useAuth } from './useAuth.js';
6
- import '@zitadel/api/config';
7
- import 'next/server';
8
- import 'react/jsx-runtime';
9
- import 'react';
1
+ /**
2
+ * Package root — for **Server Components and server modules only**: it
3
+ * includes the server-only `auth()`, so importing it from a `"use client"`
4
+ * module fails at build time. Client components import from
5
+ * `@zitadel/sdk-next/react` (provider + hooks) or
6
+ * `@zitadel/sdk-next/session` (`getSession()`) instead.
7
+ */
8
+ export * from "./types.js";
9
+ export { nextgenMiddleware, createProxy } from "./middleware.js";
10
+ export type { ProxyOptions, ProxyHandler } from "./middleware.js";
11
+ export { auth } from "./auth.js";
12
+ export type { AuthOptions } from "./auth.js";
13
+ export { NextgenProvider } from "./provider.js";
14
+ export { AuthContextProvider, useAuthContext } from "./context.js";
15
+ export { useAuth } from "./useAuth.js";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACjE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -1,24 +1,13 @@
1
- import {
2
- createProxy,
3
- nextgenMiddleware
4
- } from "./chunk-4KENHIG4.js";
5
- import {
6
- auth
7
- } from "./chunk-B7S6XMT3.js";
8
- import "./chunk-OCZMYSFX.js";
9
- import "./chunk-6F4PWJZI.js";
10
- import {
11
- useAuth
12
- } from "./chunk-XTCHTAIQ.js";
13
- import {
14
- NextgenProvider,
15
- useAuthContext
16
- } from "./chunk-2BFQLJQE.js";
17
- export {
18
- NextgenProvider,
19
- auth,
20
- createProxy,
21
- nextgenMiddleware,
22
- useAuth,
23
- useAuthContext
24
- };
1
+ /**
2
+ * Package root — for **Server Components and server modules only**: it
3
+ * includes the server-only `auth()`, so importing it from a `"use client"`
4
+ * module fails at build time. Client components import from
5
+ * `@zitadel/sdk-next/react` (provider + hooks) or
6
+ * `@zitadel/sdk-next/session` (`getSession()`) instead.
7
+ */
8
+ export * from "./types.js";
9
+ export { nextgenMiddleware, createProxy } from "./middleware.js";
10
+ export { auth } from "./auth.js";
11
+ export { NextgenProvider } from "./provider.js";
12
+ export { AuthContextProvider, useAuthContext } from "./context.js";
13
+ export { useAuth } from "./useAuth.js";
package/dist/jsx.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ // Types-only shim: scaffolded apps depend on @zitadel/sdk-next, not
2
+ // @zitadel/components, so the React JSX declarations for the <zitadel-*>
3
+ // elements are re-exposed here as `@zitadel/sdk-next/jsx`. Copied verbatim
4
+ // into dist/ by the build script.
5
+ /// <reference types="@zitadel/components/jsx" />
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Re-exports shared JWT verification from `@zitadel/sdk-core`.
3
+ *
4
+ * The JWT module is runtime-agnostic (uses `atob` which is available in both
5
+ * Edge and Node.js 16+) and is defined once in sdk-core.
6
+ */
7
+ export { JWKS_TTL_MS, base64UrlDecode, decodeJwt, isJwtShaped, verifyJwt, } from "@zitadel/sdk-core/jwt";
8
+ export type { JwtPayload, JwtHeader, DecodedJwt, VerifyJwtOptions } from "@zitadel/sdk-core/jwt";
9
+ //# sourceMappingURL=jwt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/lib/jwt.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,WAAW,EACX,eAAe,EACf,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Re-exports shared JWT verification from `@zitadel/sdk-core`.
3
+ *
4
+ * The JWT module is runtime-agnostic (uses `atob` which is available in both
5
+ * Edge and Node.js 16+) and is defined once in sdk-core.
6
+ */
7
+ export { JWKS_TTL_MS, base64UrlDecode, decodeJwt, isJwtShaped, verifyJwt, } from "@zitadel/sdk-core/jwt";
@@ -1,7 +1,7 @@
1
- import { ZitadelProject } from '@zitadel/api/config';
2
- import { NextgenMiddlewareOptions } from '@zitadel/sdk-core/types';
3
- import { NextRequest, NextResponse } from 'next/server';
4
-
1
+ import type { ZitadelProject } from "@zitadel/api/config";
2
+ import type { NextgenMiddlewareOptions } from "@zitadel/sdk-core/middleware";
3
+ import type { NextRequest } from "next/server";
4
+ import { NextResponse } from "next/server";
5
5
  /**
6
6
  * Next.js Edge middleware that handles proxying, JWT verification, and route
7
7
  * protection in a single pass.
@@ -20,6 +20,9 @@ import { NextRequest, NextResponse } from 'next/server';
20
20
  * });
21
21
  * }
22
22
  *
23
+ * A session counts only in the app's own project: `projectId`, else
24
+ * `ZITADEL_PROJECT_ID`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`.
25
+ *
23
26
  * export const config = {
24
27
  * matcher: ["/__nextgen/:path*", "/profile/:path*"],
25
28
  * };
@@ -29,7 +32,7 @@ import { NextRequest, NextResponse } from 'next/server';
29
32
  * @param options - Middleware configuration options.
30
33
  * @returns A `NextResponse` or `Response` to continue, redirect, or proxy.
31
34
  */
32
- declare function nextgenMiddleware(req: NextRequest, options?: NextgenMiddlewareOptions): Promise<NextResponse | Response>;
35
+ export declare function nextgenMiddleware(req: NextRequest, options?: NextgenMiddlewareOptions): Promise<NextResponse | Response>;
33
36
  /**
34
37
  * Options for {@link createProxy} that are separate from the shared
35
38
  * {@link ZitadelConfig}. These configure route protection, login
@@ -38,11 +41,11 @@ declare function nextgenMiddleware(req: NextRequest, options?: NextgenMiddleware
38
41
  * `proxyPath` and `url` are omitted because they come from the
39
42
  * {@link ZitadelConfig} passed as the first argument.
40
43
  */
41
- type ProxyOptions = Omit<NextgenMiddlewareOptions, 'proxyPath' | 'url'>;
44
+ export type ProxyOptions = Omit<NextgenMiddlewareOptions, "proxyPath" | "url">;
42
45
  /**
43
46
  * A pre-configured middleware handler returned by {@link createProxy}.
44
47
  */
45
- type ProxyHandler = (req: NextRequest) => Promise<NextResponse | Response>;
48
+ export type ProxyHandler = (req: NextRequest) => Promise<NextResponse | Response>;
46
49
  /**
47
50
  * Creates a pre-configured middleware handler from the SDK config
48
51
  * returned by `configureZitadel()`. This is the derived-service
@@ -54,7 +57,7 @@ type ProxyHandler = (req: NextRequest) => Promise<NextResponse | Response>;
54
57
  * import { createProxy } from "@zitadel/sdk-next/middleware";
55
58
  *
56
59
  * const zitadel = configureZitadel({
57
- * projectId: "demo",
60
+ * projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
58
61
  * url: process.env.ZITADEL_URL,
59
62
  * });
60
63
  *
@@ -71,10 +74,15 @@ type ProxyHandler = (req: NextRequest) => Promise<NextResponse | Response>;
71
74
  * export const middleware = proxy;
72
75
  * ```
73
76
  *
77
+ * Sessions are held to the handle's project unless `options.projectId` names
78
+ * another. `auth()` runs in the server runtime, where the middleware's handle
79
+ * may not be configured: it reads `ZITADEL_PROJECT_ID` /
80
+ * `NEXT_PUBLIC_ZITADEL_PROJECT_ID`, else a handle configured there. When the
81
+ * project lives only in this handle, pass it as `auth({ projectId })` too.
82
+ *
74
83
  * @param config - The SDK handle from `configureZitadel()`.
75
84
  * @param options - Route protection and JWT options.
76
85
  * @returns A middleware handler function.
77
86
  */
78
- declare function createProxy(config: ZitadelProject, options?: ProxyOptions): ProxyHandler;
79
-
80
- export { type ProxyHandler, type ProxyOptions, createProxy, nextgenMiddleware };
87
+ export declare function createProxy(config: ZitadelProject, options?: ProxyOptions): ProxyHandler;
88
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,wBAAwB,EAAyB,MAAM,8BAA8B,CAAC;AAEpG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAa/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAmC3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,WAAW,EAChB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,YAAY,GAAG,QAAQ,CAAC,CAyDlC;AAuMD;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,wBAAwB,EAAE,WAAW,GAAG,KAAK,CAAC,CAAC;AAE/E;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,GAAG,QAAQ,CAAC,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,YAAiB,GAAG,YAAY,CAQ5F"}
@@ -1,9 +1,304 @@
1
- import {
2
- createProxy,
3
- nextgenMiddleware
4
- } from "./chunk-4KENHIG4.js";
5
- import "./chunk-OCZMYSFX.js";
6
- export {
7
- createProxy,
8
- nextgenMiddleware
9
- };
1
+ import { joinURL } from "ufo";
2
+ import { HOP_BY_HOP, INTERNAL_HEADERS, filterResponseHeaders, loginRedirectUrl, matchesRoutes, proxySessionCookies, SESSION_COOKIE_CACHE_CONTROL, requiresProjectSecret, resolveSession, } from "@zitadel/sdk-core/middleware";
3
+ import { NextResponse } from "next/server";
4
+ /**
5
+ * Clones the incoming request headers, injects `extra` key/value pairs,
6
+ * and registers them with the Next.js header-tunnelling mechanism so that
7
+ * server components can read them via `headers()`.
8
+ *
9
+ * @param req - The incoming edge request.
10
+ * @param extra - Additional headers to inject (e.g. `x-nextgen-auth-token`).
11
+ * @returns A new `Headers` instance with the injected values registered.
12
+ */
13
+ function tunnelHeaders(req, extra) {
14
+ const headers = new Headers(req.headers);
15
+ // Security: strip any x-middleware-override-headers the client may have sent.
16
+ // Preserving the client value would allow an attacker to inject arbitrary
17
+ // header names into the Next.js override list, potentially bypassing the
18
+ // internal header-tunnelling safety checks. We always build this header from
19
+ // scratch using only the names we inject ourselves.
20
+ headers.delete("x-middleware-override-headers");
21
+ const injectedNames = [];
22
+ // This is the only writer of x-nextgen-auth-token: it injects the verified
23
+ // value for server components, and a client-sent one never reaches here.
24
+ for (const [name, value] of Object.entries(extra)) {
25
+ headers.set(name, value);
26
+ headers.set(`x-middleware-request-${name}`, value);
27
+ injectedNames.push(name);
28
+ }
29
+ headers.set("x-middleware-override-headers", injectedNames.join(","));
30
+ return headers;
31
+ }
32
+ /**
33
+ * Next.js Edge middleware that handles proxying, JWT verification, and route
34
+ * protection in a single pass.
35
+ *
36
+ * Place this in your `middleware.ts` file:
37
+ *
38
+ * ```ts
39
+ * import { nextgenMiddleware } from "@zitadel/sdk-next/middleware";
40
+ * import type { NextRequest } from "next/server";
41
+ *
42
+ * export function middleware(req: NextRequest) {
43
+ * return nextgenMiddleware(req, {
44
+ * url: process.env.ZITADEL_URL,
45
+ * protectedRoutes: ["/profile"],
46
+ * loginPath: "/login",
47
+ * });
48
+ * }
49
+ *
50
+ * A session counts only in the app's own project: `projectId`, else
51
+ * `ZITADEL_PROJECT_ID`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`.
52
+ *
53
+ * export const config = {
54
+ * matcher: ["/__nextgen/:path*", "/profile/:path*"],
55
+ * };
56
+ * ```
57
+ *
58
+ * @param req - The incoming Next.js edge request.
59
+ * @param options - Middleware configuration options.
60
+ * @returns A `NextResponse` or `Response` to continue, redirect, or proxy.
61
+ */
62
+ export async function nextgenMiddleware(req, options = {}) {
63
+ const { url = process.env.ZITADEL_URL ?? "http://localhost:8080", projectId = process.env.ZITADEL_PROJECT_ID || process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID, proxyPath = "/__nextgen", protectedRoutes = [], ignoredRoutes = [], loginPath = "/login", allowedAlgorithms = ["RS256", "ES256"], clockSkewMs = 5000, audience, allowedTokenTypes = ["JWT", "at+JWT"], jwksTimeoutMs, proxyTimeoutMs = 5000, opaqueTokenTimeoutMs = 5000, } = options;
64
+ // Guard against open-redirect: loginPath must be a relative path. An absolute
65
+ // URL (e.g. "https://evil.com/phish") or a protocol-relative URL
66
+ // (e.g. "//evil.com") would redirect the browser to an external host.
67
+ if (!loginPath.startsWith("/") || loginPath.startsWith("//")) {
68
+ throw new Error(`[nextgen] loginPath must be a relative path starting with a single "/". ` +
69
+ `Received: "${loginPath}". Using an absolute or protocol-relative URL ` +
70
+ `would allow open-redirect attacks.`);
71
+ }
72
+ // Clients never send x-nextgen-auth-token: it is an internal channel. Refuse any
73
+ // request that carries it, on every route, before anything else runs.
74
+ if (req.headers.has("x-nextgen-auth-token")) {
75
+ return new Response("Bad Request", { status: 400 });
76
+ }
77
+ const { pathname } = new URL(req.url);
78
+ if (matchesRoutes(pathname, ignoredRoutes)) {
79
+ return NextResponse.next();
80
+ }
81
+ if (pathname === proxyPath || pathname.startsWith(`${proxyPath}/`)) {
82
+ return proxyRequest(req, url, proxyPath, projectId, proxyTimeoutMs);
83
+ }
84
+ return handleAuth(req, {
85
+ url,
86
+ projectId,
87
+ protectedRoutes,
88
+ loginPath,
89
+ allowedAlgorithms,
90
+ clockSkewMs,
91
+ audience,
92
+ allowedTokenTypes,
93
+ jwksTimeoutMs,
94
+ opaqueTokenTimeoutMs,
95
+ pathname,
96
+ });
97
+ }
98
+ /**
99
+ * Forwards a `/__nextgen/*` request to the upstream auth backend and streams
100
+ * the response back, stripping hop-by-hop headers in both directions. The
101
+ * session cookie travels under this project's name in the browser and the
102
+ * server's name upstream ({@link proxySessionCookies}).
103
+ *
104
+ * All non-hop-by-hop headers from the incoming request are forwarded, including
105
+ * any `X-Forwarded-For` chain already set by an upstream CDN or load balancer.
106
+ * When `X-Forwarded-For` is absent (direct connection with no upstream proxy),
107
+ * it is initialised from the runtime-provided client IP so the auth server
108
+ * always receives origin information. `X-Forwarded-Host` and `X-Forwarded-Proto`
109
+ * are set only when absent, preserving values injected by an upstream CDN.
110
+ *
111
+ * @param req - The incoming edge request.
112
+ * @param authUrl - Base URL of the auth backend.
113
+ * @param proxyPath - The path prefix being proxied (e.g. `"/__nextgen"`).
114
+ * @param projectId - The project this app signs its users in to.
115
+ * @returns The proxied upstream `Response`.
116
+ */
117
+ async function proxyRequest(req, authUrl, proxyPath, projectId, proxyTimeoutMs) {
118
+ const url = new URL(req.url);
119
+ const suffix = url.pathname.slice(proxyPath.length);
120
+ const target = `${joinURL(authUrl, suffix)}${url.search}`;
121
+ const upstreamHeaders = new Headers();
122
+ for (const [key, value] of req.headers.entries()) {
123
+ const lower = key.toLowerCase();
124
+ if (!HOP_BY_HOP.has(lower) && !INTERNAL_HEADERS.has(lower)) {
125
+ upstreamHeaders.set(key, value);
126
+ }
127
+ }
128
+ // Always append the direct client IP to the X-Forwarded-For chain so the
129
+ // upstream auth server sees the full proxy path. We never skip this even when
130
+ // a chain is already present — a CDN or load balancer may have set XFF before
131
+ // the request reached this edge node, and our hop must still be recorded.
132
+ const directIp = req.ip;
133
+ if (directIp) {
134
+ const existingXff = upstreamHeaders.get("x-forwarded-for");
135
+ upstreamHeaders.set("x-forwarded-for", existingXff ? `${existingXff}, ${directIp}` : directIp);
136
+ }
137
+ if (!upstreamHeaders.has("x-forwarded-host")) {
138
+ upstreamHeaders.set("x-forwarded-host", url.host);
139
+ }
140
+ if (!upstreamHeaders.has("x-forwarded-proto")) {
141
+ upstreamHeaders.set("x-forwarded-proto", url.protocol.replace(":", ""));
142
+ }
143
+ const sessionCookies = proxySessionCookies({
144
+ method: req.method,
145
+ path: suffix,
146
+ search: url.search,
147
+ cookie: req.headers.get("cookie"),
148
+ requestUrl: url,
149
+ forwardedProto: req.headers.get("x-forwarded-proto"),
150
+ forwardedHost: req.headers.get("x-forwarded-host"),
151
+ }, projectId, { url: authUrl, timeoutMs: proxyTimeoutMs });
152
+ if (sessionCookies.upstreamCookie === null) {
153
+ upstreamHeaders.delete("cookie");
154
+ }
155
+ else {
156
+ upstreamHeaders.set("cookie", sessionCookies.upstreamCookie);
157
+ }
158
+ const hasBody = !["GET", "HEAD"].includes(req.method);
159
+ // ADR 036: the current browser flow needs the confidential project secret
160
+ // only for the handoff exchange. Stamping it onto every proxied operation
161
+ // turns this public request boundary into an operator-capable open relay.
162
+ // Preserve an explicit caller credential so the exchange can use the future
163
+ // publishable-key plane without being overwritten.
164
+ if (requiresProjectSecret(req.method, suffix) && !upstreamHeaders.has("authorization")) {
165
+ const secret = process.env.ZITADEL_PROJECT_SECRET;
166
+ if (secret) {
167
+ upstreamHeaders.set("authorization", `Bearer ${secret}`);
168
+ }
169
+ }
170
+ // Read body eagerly so undici receives a concrete buffer rather than a
171
+ // WinterCG ReadableStream, which is incompatible with Node.js fetch body
172
+ // extraction.
173
+ const bodyBuffer = hasBody ? await req.arrayBuffer() : undefined;
174
+ const upstream = await fetch(target, {
175
+ method: req.method,
176
+ headers: upstreamHeaders,
177
+ body: bodyBuffer,
178
+ redirect: "manual",
179
+ signal: AbortSignal.timeout(proxyTimeoutMs),
180
+ });
181
+ // The app's own request URL: the origin a proxied redirect may name, and
182
+ // the base a relative one resolves against.
183
+ const responseHeaders = filterResponseHeaders(upstream.headers, req.nextUrl.toString());
184
+ const answer = await sessionCookies.toBrowser(upstream);
185
+ for (const cookie of answer.setCookies) {
186
+ responseHeaders.append("set-cookie", cookie);
187
+ }
188
+ if (answer.setCookies.length > 0) {
189
+ responseHeaders.set("cache-control", SESSION_COOKIE_CACHE_CONTROL);
190
+ }
191
+ return new Response(answer.body, {
192
+ status: upstream.status,
193
+ headers: responseHeaders,
194
+ });
195
+ }
196
+ /**
197
+ * Judges the session and tunnels the token of a signed-in user to server
198
+ * components via a request header, where `auth()` verifies it again.
199
+ * Redirects to the login page when nobody is signed in on a protected route.
200
+ * Either way the response carries the session cookies the judgement asks for.
201
+ * When the backend cannot say whether the session is live, a protected route
202
+ * answers `503` instead.
203
+ *
204
+ * @param req - The incoming edge request.
205
+ * @param opts - Auth handler options.
206
+ * @returns A `NextResponse` to continue, redirect, or answer `503`.
207
+ */
208
+ async function handleAuth(req, opts) {
209
+ const { protectedRoutes, loginPath, pathname, ...verification } = opts;
210
+ const authHeader = req.headers.get("authorization");
211
+ const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
212
+ const { auth, undecided, cookies } = await resolveSession({
213
+ bearer: bearerToken,
214
+ cookie: (name) => req.cookies.get(name)?.value,
215
+ requestUrl: req.url,
216
+ forwardedProto: req.headers.get("x-forwarded-proto"),
217
+ forwardedHost: req.headers.get("x-forwarded-host"),
218
+ }, verification);
219
+ let response;
220
+ if (auth.isAuthenticated) {
221
+ const tunnelled = tunnelHeaders(req, { "x-nextgen-auth-token": auth.session.token });
222
+ response = NextResponse.next({ request: { headers: tunnelled } });
223
+ }
224
+ else if (undecided && matchesRoutes(pathname, protectedRoutes)) {
225
+ // The backend could not say whether the session is live. The user may well
226
+ // be signed in, so a protected route answers 503 rather than send them to
227
+ // the login page.
228
+ response = new NextResponse("Service Unavailable", { status: 503 });
229
+ }
230
+ else if (matchesRoutes(pathname, protectedRoutes)) {
231
+ response = NextResponse.redirect(loginRedirectUrl(loginPath, req.url), { status: 302 });
232
+ }
233
+ else {
234
+ const tunnelled = tunnelHeaders(req, { "x-nextgen-auth-token": "" });
235
+ response = NextResponse.next({ request: { headers: tunnelled } });
236
+ }
237
+ // A dead session's cookie is cleared so the browser stops replaying it, and a
238
+ // session of this project still under the shared name is copied to the
239
+ // project's own cookie. A live session that signs no one in here (an
240
+ // anonymous one, or one of another project) keeps its cookie, and so does one
241
+ // the backend gave no verdict on. The cookies go through the response's
242
+ // cookie jar, so a middleware composed around this one that sets a cookie of
243
+ // its own through it keeps them.
244
+ for (const cookie of cookies) {
245
+ response.cookies.set(cookie.name, cookie.value, {
246
+ path: "/",
247
+ httpOnly: true,
248
+ sameSite: "lax",
249
+ secure: cookie.secure,
250
+ ...(cookie.maxAge === undefined ? {} : { maxAge: cookie.maxAge }),
251
+ });
252
+ }
253
+ if (cookies.length > 0) {
254
+ response.headers.set("cache-control", SESSION_COOKIE_CACHE_CONTROL);
255
+ }
256
+ return response;
257
+ }
258
+ /**
259
+ * Creates a pre-configured middleware handler from the SDK config
260
+ * returned by `configureZitadel()`. This is the derived-service
261
+ * derived service pattern:
262
+ *
263
+ * ```ts
264
+ * // src/zitadel.ts
265
+ * import { configureZitadel } from "@zitadel/api/config";
266
+ * import { createProxy } from "@zitadel/sdk-next/middleware";
267
+ *
268
+ * const zitadel = configureZitadel({
269
+ * projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
270
+ * url: process.env.ZITADEL_URL,
271
+ * });
272
+ *
273
+ * export const proxy = createProxy(zitadel, {
274
+ * protectedRoutes: ["/admin*"],
275
+ * loginPath: "/login",
276
+ * });
277
+ * ```
278
+ *
279
+ * Then in middleware.ts:
280
+ *
281
+ * ```ts
282
+ * import { proxy } from "./zitadel";
283
+ * export const middleware = proxy;
284
+ * ```
285
+ *
286
+ * Sessions are held to the handle's project unless `options.projectId` names
287
+ * another. `auth()` runs in the server runtime, where the middleware's handle
288
+ * may not be configured: it reads `ZITADEL_PROJECT_ID` /
289
+ * `NEXT_PUBLIC_ZITADEL_PROJECT_ID`, else a handle configured there. When the
290
+ * project lives only in this handle, pass it as `auth({ projectId })` too.
291
+ *
292
+ * @param config - The SDK handle from `configureZitadel()`.
293
+ * @param options - Route protection and JWT options.
294
+ * @returns A middleware handler function.
295
+ */
296
+ export function createProxy(config, options = {}) {
297
+ const mergedOptions = {
298
+ ...options,
299
+ projectId: options.projectId ?? (config.projectId || undefined),
300
+ proxyPath: config.proxyPath,
301
+ url: config.url,
302
+ };
303
+ return (req) => nextgenMiddleware(req, mergedOptions);
304
+ }