@zitadel/sdk-next 1.0.0-alpha.24 → 1.0.0-alpha.28
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.
- package/README.md +86 -17
- package/dist/auth.d.ts +14 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +59 -51
- package/dist/client.d.ts +7 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -3
- package/dist/middleware.d.ts +10 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +100 -92
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/session.d.ts +26 -10
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +55 -15
- package/package.json +17 -26
package/README.md
CHANGED
|
@@ -5,9 +5,19 @@ Next.js middleware and helpers for Nextgen Auth.
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
pnpm add @zitadel/sdk-next
|
|
8
|
+
pnpm add @zitadel/sdk-next@alpha
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Requires Next.js 15.2.3 or later. Earlier 15.x releases skip middleware for a
|
|
12
|
+
request that sends an `x-middleware-subrequest` header (CVE-2025-29927), which
|
|
13
|
+
on self-hosted deployments would let any request past `protectedRoutes`.
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
Peer dependencies: `next >=15.2.3`, `react >=18` and `react-dom >=18`.
|
|
18
|
+
|
|
19
|
+
TypeScript ≥ 5.0, the floor shared by every SDK; see the [compatibility notes](https://github.com/zitadel/nextgen/blob/main/packages/AGENTS.md).
|
|
20
|
+
|
|
11
21
|
## Entry points
|
|
12
22
|
|
|
13
23
|
| Import | Runs in | Provides |
|
|
@@ -52,20 +62,33 @@ The middleware runs on every matched route and does three things in one pass:
|
|
|
52
62
|
|
|
53
63
|
1. **Proxies** `/__nextgen/*` requests to the auth backend
|
|
54
64
|
2. **Verifies** the session JWT via JWKS using the Web Crypto API
|
|
55
|
-
3. **Redirects** unauthenticated requests to `loginPath` for protected routes
|
|
65
|
+
3. **Redirects** unauthenticated requests to `loginPath` for protected routes, with the requested path and query string in `?next=`, where `<zitadel-login>` returns after sign-in
|
|
56
66
|
|
|
57
67
|
### 2. Reading auth in a Server Component
|
|
58
68
|
|
|
59
|
-
```
|
|
69
|
+
```tsx
|
|
60
70
|
import { auth } from "@zitadel/sdk-next/server";
|
|
71
|
+
import { Suspense } from "react";
|
|
72
|
+
|
|
73
|
+
export default function Page() {
|
|
74
|
+
return (
|
|
75
|
+
<Suspense fallback={<p>Loading…</p>}>
|
|
76
|
+
<Greeting />
|
|
77
|
+
</Suspense>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
61
80
|
|
|
62
|
-
|
|
81
|
+
async function Greeting() {
|
|
63
82
|
const session = await auth();
|
|
64
83
|
if (!session.isAuthenticated) return <p>Not signed in</p>;
|
|
65
84
|
return <p>Hello {session.session.display ?? session.session.identifier}</p>;
|
|
66
85
|
}
|
|
67
86
|
```
|
|
68
87
|
|
|
88
|
+
`auth()` reads the request, so whatever awaits it renders inside
|
|
89
|
+
`<Suspense>`: with Cache Components on (`cacheComponents`, which
|
|
90
|
+
`create-next-app` enables), Next.js refuses request data outside it.
|
|
91
|
+
|
|
69
92
|
`auth()` reads the token the middleware tunnelled into the request headers
|
|
70
93
|
and **verifies it before trusting it** — JWTs cryptographically via JWKS,
|
|
71
94
|
opaque tokens against the backend's `GET /sessions/me` (which also supplies
|
|
@@ -91,17 +114,24 @@ read the state with `useAuth()` anywhere below it:
|
|
|
91
114
|
|
|
92
115
|
```tsx
|
|
93
116
|
import { auth, NextgenProvider } from '@zitadel/sdk-next/server';
|
|
117
|
+
import { Suspense } from 'react';
|
|
94
118
|
|
|
95
|
-
export default
|
|
96
|
-
const session = await auth();
|
|
119
|
+
export default function RootLayout({ children }) {
|
|
97
120
|
return (
|
|
98
121
|
<html>
|
|
99
122
|
<body>
|
|
100
|
-
<
|
|
123
|
+
<Suspense>
|
|
124
|
+
<AuthProvider>{children}</AuthProvider>
|
|
125
|
+
</Suspense>
|
|
101
126
|
</body>
|
|
102
127
|
</html>
|
|
103
128
|
);
|
|
104
129
|
}
|
|
130
|
+
|
|
131
|
+
async function AuthProvider({ children }) {
|
|
132
|
+
const session = await auth();
|
|
133
|
+
return <NextgenProvider session={session}>{children}</NextgenProvider>;
|
|
134
|
+
}
|
|
105
135
|
```
|
|
106
136
|
|
|
107
137
|
`NextgenProvider` converts the `auth()` result to the client-safe shape
|
|
@@ -144,8 +174,10 @@ scaffolded matcher covers just the proxy path and the protected routes — so a
|
|
|
144
174
|
header on a public page would always look signed out. For your app's own chrome
|
|
145
175
|
(header navigation, account menus), read the session client-side with
|
|
146
176
|
`getSession()` from `@zitadel/sdk-next/session`. It fetches the same-origin
|
|
147
|
-
`{proxyPath}/sessions/me
|
|
148
|
-
so it works on every page and the answer is the server's
|
|
177
|
+
`{proxyPath}/sessions/me?project_id=…` — the same read the `<zitadel-session>`
|
|
178
|
+
card performs — so it works on every page and the answer is the server's. The
|
|
179
|
+
project is the `projectId` option, else the project of `configureZitadel()`,
|
|
180
|
+
else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`; with none, it reads as signed out:
|
|
149
181
|
|
|
150
182
|
```tsx
|
|
151
183
|
'use client';
|
|
@@ -157,7 +189,7 @@ export function HeaderNav() {
|
|
|
157
189
|
const [auth, setAuth] = useState<ClientAuthResult>();
|
|
158
190
|
const [error, setError] = useState<Error>();
|
|
159
191
|
useEffect(() => {
|
|
160
|
-
getSession().then(setAuth, setError);
|
|
192
|
+
getSession({ projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID }).then(setAuth, setError);
|
|
161
193
|
}, []);
|
|
162
194
|
if (error) return <span role="alert">Session unavailable</span>;
|
|
163
195
|
if (!auth) return null;
|
|
@@ -174,8 +206,9 @@ A rejected `getSession()` means the state is *unknown* (broken proxy, network,
|
|
|
174
206
|
|
|
175
207
|
A `200` with a non-empty `user_id` resolves to
|
|
176
208
|
`{ isAuthenticated: true, session: { userId, email, name } }` (client-safe —
|
|
177
|
-
no token); the canonical `401/auth.unauthorized`,
|
|
178
|
-
`404/sess.not_found`, and
|
|
209
|
+
no token); the canonical `401/auth.unauthorized`, `403/sess.project_mismatch`
|
|
210
|
+
(a session of another project on the same host), `404/sess.not_found`, and
|
|
211
|
+
anonymous sessions resolve to signed out. The request
|
|
179
212
|
and response are both marked no-store. Any other response — including malformed
|
|
180
213
|
JSON or a framework's HTML 404 page from a misrouted proxy — throws so a broken
|
|
181
214
|
proxy doesn't silently render as signed out. Sign-in and sign-out navigate
|
|
@@ -189,11 +222,20 @@ The `<zitadel-login>` web component (from `@zitadel/components`) must be rendere
|
|
|
189
222
|
|
|
190
223
|
```tsx
|
|
191
224
|
// app/login/page.tsx (server)
|
|
192
|
-
import { auth } from '@zitadel/sdk-next';
|
|
225
|
+
import { auth } from '@zitadel/sdk-next/server';
|
|
193
226
|
import { redirect } from 'next/navigation';
|
|
227
|
+
import { Suspense } from 'react';
|
|
194
228
|
import { LoginWidget } from './widget';
|
|
195
229
|
|
|
196
|
-
export default
|
|
230
|
+
export default function LoginPage() {
|
|
231
|
+
return (
|
|
232
|
+
<Suspense>
|
|
233
|
+
<SignedOutOnly />
|
|
234
|
+
</Suspense>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function SignedOutOnly() {
|
|
197
239
|
const session = await auth();
|
|
198
240
|
if (session.isAuthenticated) redirect('/admin');
|
|
199
241
|
return <LoginWidget />;
|
|
@@ -232,12 +274,39 @@ There is no `api-base` attribute — the element reads the global handle from
|
|
|
232
274
|
`configureZitadel()` (or you can assign the returned handle to the element's
|
|
233
275
|
`project` property).
|
|
234
276
|
|
|
277
|
+
## Widget events
|
|
278
|
+
|
|
279
|
+
The widgets are plain custom elements, so each event is a DOM `CustomEvent` that carries its payload in `detail`. The events and payloads are the same ones every SPA SDK exposes as callbacks.
|
|
280
|
+
|
|
281
|
+
| Widget event | Fires when | Detail | Widgets |
|
|
282
|
+
| --- | --- | --- | --- |
|
|
283
|
+
| `zitadel-flow-step` | Login shows a step, the first one included. | `{ step }` | Login |
|
|
284
|
+
| `zitadel-flow-input` | The visitor edits a field. | `{ name, value }` | Login |
|
|
285
|
+
| `zitadel-flow-complete` | The flow reaches a terminal step. | `{ behavior, redirect_uri?, handoff_token?, handoff_token_expires_at? }` | Login |
|
|
286
|
+
| `zitadel-flow-redirect` | The widget hands the browser to an identity provider; nobody is signed in yet. | `{ redirect_url, step }` | Login |
|
|
287
|
+
| `zitadel-flow-error` | The flow reports an error. | `{ message }` | Login |
|
|
288
|
+
| `zitadel-signout` | The user signs out. | `{ display, identifier }` | Logout, Session |
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
const ref = useRef<HTMLElementTagNameMap['zitadel-login']>(null);
|
|
292
|
+
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
const login = ref.current;
|
|
295
|
+
const onComplete = (event: Event) => track((event as CustomEvent).detail);
|
|
296
|
+
login?.addEventListener('zitadel-flow-complete', onComplete);
|
|
297
|
+
return () => login?.removeEventListener('zitadel-flow-complete', onComplete);
|
|
298
|
+
}, []);
|
|
299
|
+
|
|
300
|
+
return <zitadel-login ref={ref} post-sign-in-url="/admin" />;
|
|
301
|
+
```
|
|
302
|
+
|
|
235
303
|
## Middleware options
|
|
236
304
|
|
|
237
305
|
| Option | Type | Default | Description |
|
|
238
306
|
| ------------------- | -------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
|
|
239
|
-
| `url` | `string` | `ZITADEL_URL` env | Full URL of the Zitadel auth backend
|
|
240
|
-
| `
|
|
307
|
+
| `url` | `string` | `ZITADEL_URL` env | Full URL of the Zitadel auth backend. A trailing slash is ignored. |
|
|
308
|
+
| `projectId` | `string` | `ZITADEL_PROJECT_ID`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID` | The project this app signs its users in to. A session of any other project on the server reads as signed out and keeps its cookie; with no project, no session counts. Pass the same value to `auth()` |
|
|
309
|
+
| `proxyPath` | `string` | `"/__nextgen"` | Path prefix proxied to the auth backend. Sign-in with an external provider currently requires the default: the provider returns to `/__nextgen/idp/{slug}/callback` |
|
|
241
310
|
| `protectedRoutes` | `string[]` | `[]` | Paths requiring a valid session. Trailing `*` matches sub-paths |
|
|
242
311
|
| `ignoredRoutes` | `string[]` | `[]` | Paths skipped entirely — no JWT check, no tunnelling. Useful for webhooks or health checks. Trailing `*` matches sub-paths |
|
|
243
312
|
| `loginPath` | `string` | `"/login"` | Where to redirect unauthenticated users |
|
|
@@ -256,5 +325,5 @@ The verification pipeline is shared across SDKs and documented once in
|
|
|
256
325
|
[`@zitadel/sdk-core`](https://github.com/zitadel/nextgen/tree/main/packages/sdk-core#how-jwt-verification-works).
|
|
257
326
|
On top of that pipeline, the Next.js middleware adds:
|
|
258
327
|
|
|
259
|
-
1.
|
|
328
|
+
1. A request that carries an `x-nextgen-auth-token` header is refused with a 400 on every route, including ignored routes and the proxy path: the header is an internal channel that only the middleware writes, so a client never sends it
|
|
260
329
|
2. `auth()` re-applies the same verification to the tunnelled token in the server runtime — the header alone is never treated as proof of a session
|
package/dist/auth.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { AuthResult, NextgenMiddlewareOptions } from "./types.js";
|
|
|
9
9
|
* a token the middleware accepted can fail re-verification in `auth()` and
|
|
10
10
|
* render as signed out.
|
|
11
11
|
*/
|
|
12
|
-
export type AuthOptions = Pick<NextgenMiddlewareOptions, "url" | "allowedAlgorithms" | "clockSkewMs" | "audience" | "allowedTokenTypes" | "jwksTimeoutMs" | "opaqueTokenTimeoutMs">;
|
|
12
|
+
export type AuthOptions = Pick<NextgenMiddlewareOptions, "url" | "projectId" | "allowedAlgorithms" | "clockSkewMs" | "audience" | "allowedTokenTypes" | "jwksTimeoutMs" | "opaqueTokenTimeoutMs">;
|
|
13
13
|
/**
|
|
14
14
|
* Reads the auth state in a React Server Component or Next.js Route Handler.
|
|
15
15
|
*
|
|
@@ -27,10 +27,22 @@ export type AuthOptions = Pick<NextgenMiddlewareOptions, "url" | "allowedAlgorit
|
|
|
27
27
|
* `cache()`.
|
|
28
28
|
*
|
|
29
29
|
* The header alone is never proof of anything: on routes the middleware
|
|
30
|
-
* `matcher` does not cover, the middleware cannot
|
|
30
|
+
* `matcher` does not cover, the middleware cannot refuse a forged
|
|
31
31
|
* client-supplied header, so `auth()` re-verifies every value it reads.
|
|
32
32
|
* A header value that fails verification is treated as signed out.
|
|
33
33
|
*
|
|
34
|
+
* Either way a session counts only in the app's own project: `projectId`,
|
|
35
|
+
* else `ZITADEL_PROJECT_ID`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`, else the
|
|
36
|
+
* project of `configureZitadel()` when it has run in this server runtime. A
|
|
37
|
+
* JWT must name it in `aud`; an opaque token must be a session of that
|
|
38
|
+
* project. With no project, nobody is signed in. A middleware built with
|
|
39
|
+
* `createProxy(config)` holds sessions to `config.projectId`: pass the same
|
|
40
|
+
* project here unless it comes from one of those variables.
|
|
41
|
+
*
|
|
42
|
+
* A backend that answers with an unexpected status, or not at all, reads as
|
|
43
|
+
* signed out and is logged, so a broken proxy or an unreachable backend is
|
|
44
|
+
* distinguishable from a missing session.
|
|
45
|
+
*
|
|
34
46
|
* **Matcher precondition (read this):** the middleware only tunnels the
|
|
35
47
|
* session token on routes covered by its `matcher`. On uncovered routes the
|
|
36
48
|
* cookie never reaches `auth()`, so `auth()` reports signed out even when a
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAQrB,OAAO,KAAK,EAAE,UAAU,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAIvE;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,IAAI,CAC5B,wBAAwB,EACtB,KAAK,GACL,WAAW,GACX,mBAAmB,GACnB,aAAa,GACb,UAAU,GACV,mBAAmB,GACnB,eAAe,GACf,sBAAsB,CACzB,CAAC;AAgBF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,wBAAsB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAsFzE"}
|
package/dist/auth.js
CHANGED
|
@@ -1,57 +1,20 @@
|
|
|
1
1
|
import "server-only";
|
|
2
|
+
import { getZitadelConfig } from "@zitadel/api/config";
|
|
3
|
+
import { checkOpaqueSessionToken, jwtNamesProject } from "@zitadel/sdk-core/middleware";
|
|
4
|
+
import { joinURL } from "ufo";
|
|
2
5
|
import { headers } from "next/headers";
|
|
3
6
|
import { cache } from "react";
|
|
4
7
|
import { isJwtShaped, verifyJwt } from "./lib/jwt.js";
|
|
5
8
|
/**
|
|
6
|
-
*
|
|
7
|
-
* `GET /sessions/me
|
|
8
|
-
*
|
|
9
|
+
* Checks an opaque (non-JWT) session token against the backend's
|
|
10
|
+
* `GET /sessions/me`, held to `projectId` (see `checkOpaqueSessionToken` in
|
|
11
|
+
* `@zitadel/sdk-core`, which the middleware uses too).
|
|
9
12
|
*
|
|
10
13
|
* Wrapped in React's `cache()` so that many components calling `auth()` in
|
|
11
14
|
* the same render pass share a single backend round-trip. Arguments are
|
|
12
15
|
* primitives on purpose — `cache()` memoises by argument identity.
|
|
13
|
-
*
|
|
14
|
-
* Fail-closed: network errors, timeouts, and unexpected statuses all resolve
|
|
15
|
-
* to `null` (signed out). Unexpected failures are logged so a broken proxy or
|
|
16
|
-
* unreachable backend is distinguishable from a genuinely missing session.
|
|
17
16
|
*/
|
|
18
|
-
const
|
|
19
|
-
try {
|
|
20
|
-
const res = await fetch(`${issuerUrl}/sessions/me`, {
|
|
21
|
-
method: "GET",
|
|
22
|
-
headers: { accept: "application/json", cookie: `__nextgen_session=${token}` },
|
|
23
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
24
|
-
});
|
|
25
|
-
// 401 = no/invalid session token, 404 = session gone (revoked/expired):
|
|
26
|
-
// both are the server's definitive "not signed in".
|
|
27
|
-
if (res.status === 401 || res.status === 404) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
if (!res.ok) {
|
|
31
|
-
console.warn(`[nextgen] auth(): session validation failed with HTTP ${res.status} from ` +
|
|
32
|
-
`${issuerUrl}/sessions/me — treating as signed out. If this persists, the ` +
|
|
33
|
-
`backend URL is likely misconfigured (ZITADEL_URL or the \`url\` option).`);
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
const session = (await res.json());
|
|
37
|
-
// An anonymous session (no verified user factor yet) has no user_id —
|
|
38
|
-
// for server-rendered UI that is "not signed in".
|
|
39
|
-
if (!session.user_id) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
userId: session.user_id,
|
|
44
|
-
identifier: session.user?.identifier ?? null,
|
|
45
|
-
identifierProperty: session.user?.identifier_property ?? null,
|
|
46
|
-
display: session.user?.display ?? null,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
console.warn(`[nextgen] auth(): could not reach ${issuerUrl}/sessions/me to validate the ` +
|
|
51
|
-
`session token — treating as signed out.`);
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
});
|
|
17
|
+
const checkOpaqueSession = cache((token, issuerUrl, timeoutMs, projectId) => checkOpaqueSessionToken(token, issuerUrl, timeoutMs, projectId));
|
|
55
18
|
/**
|
|
56
19
|
* Reads the auth state in a React Server Component or Next.js Route Handler.
|
|
57
20
|
*
|
|
@@ -69,10 +32,22 @@ const validateOpaqueSession = cache(async (token, issuerUrl, timeoutMs) => {
|
|
|
69
32
|
* `cache()`.
|
|
70
33
|
*
|
|
71
34
|
* The header alone is never proof of anything: on routes the middleware
|
|
72
|
-
* `matcher` does not cover, the middleware cannot
|
|
35
|
+
* `matcher` does not cover, the middleware cannot refuse a forged
|
|
73
36
|
* client-supplied header, so `auth()` re-verifies every value it reads.
|
|
74
37
|
* A header value that fails verification is treated as signed out.
|
|
75
38
|
*
|
|
39
|
+
* Either way a session counts only in the app's own project: `projectId`,
|
|
40
|
+
* else `ZITADEL_PROJECT_ID`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`, else the
|
|
41
|
+
* project of `configureZitadel()` when it has run in this server runtime. A
|
|
42
|
+
* JWT must name it in `aud`; an opaque token must be a session of that
|
|
43
|
+
* project. With no project, nobody is signed in. A middleware built with
|
|
44
|
+
* `createProxy(config)` holds sessions to `config.projectId`: pass the same
|
|
45
|
+
* project here unless it comes from one of those variables.
|
|
46
|
+
*
|
|
47
|
+
* A backend that answers with an unexpected status, or not at all, reads as
|
|
48
|
+
* signed out and is logged, so a broken proxy or an unreachable backend is
|
|
49
|
+
* distinguishable from a missing session.
|
|
50
|
+
*
|
|
76
51
|
* **Matcher precondition (read this):** the middleware only tunnels the
|
|
77
52
|
* session token on routes covered by its `matcher`. On uncovered routes the
|
|
78
53
|
* cookie never reaches `auth()`, so `auth()` reports signed out even when a
|
|
@@ -112,7 +87,13 @@ export async function auth(options = {}) {
|
|
|
112
87
|
if (!token) {
|
|
113
88
|
return { isAuthenticated: false, session: null };
|
|
114
89
|
}
|
|
115
|
-
const { url = process.env.ZITADEL_URL ?? "http://localhost:8080",
|
|
90
|
+
const { url = process.env.ZITADEL_URL ?? "http://localhost:8080", projectId = process.env.ZITADEL_PROJECT_ID ||
|
|
91
|
+
process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ||
|
|
92
|
+
getZitadelConfig()?.projectId, allowedAlgorithms = ["RS256", "ES256"], clockSkewMs = 5000, audience, allowedTokenTypes = ["JWT", "at+JWT"], jwksTimeoutMs, opaqueTokenTimeoutMs = 5000, } = options;
|
|
93
|
+
if (!projectId) {
|
|
94
|
+
warnMissingProject();
|
|
95
|
+
return { isAuthenticated: false, session: null };
|
|
96
|
+
}
|
|
116
97
|
if (isJwtShaped(token)) {
|
|
117
98
|
const payload = await verifyJwt(token, {
|
|
118
99
|
issuerUrl: url,
|
|
@@ -122,7 +103,7 @@ export async function auth(options = {}) {
|
|
|
122
103
|
allowedTokenTypes,
|
|
123
104
|
jwksTimeoutMs,
|
|
124
105
|
});
|
|
125
|
-
if (payload?.sub) {
|
|
106
|
+
if (payload?.sub && jwtNamesProject(payload, projectId)) {
|
|
126
107
|
return {
|
|
127
108
|
isAuthenticated: true,
|
|
128
109
|
session: {
|
|
@@ -134,6 +115,10 @@ export async function auth(options = {}) {
|
|
|
134
115
|
},
|
|
135
116
|
};
|
|
136
117
|
}
|
|
118
|
+
if (payload?.sub) {
|
|
119
|
+
// Verified, but issued for another project: not a user of this one.
|
|
120
|
+
return { isAuthenticated: false, session: null };
|
|
121
|
+
}
|
|
137
122
|
// A middleware-tunnelled JWT always verifies (the middleware applied the
|
|
138
123
|
// same checks moments earlier). Reaching this branch means the header was
|
|
139
124
|
// NOT vetted by the middleware — a route outside the `matcher` received a
|
|
@@ -143,12 +128,35 @@ export async function auth(options = {}) {
|
|
|
143
128
|
console.warn("[nextgen] auth(): rejected an x-nextgen-auth-token header that failed JWT " +
|
|
144
129
|
"verification. If this route is not covered by the middleware matcher, this was " +
|
|
145
130
|
"a forged client header; otherwise check that auth() and the middleware use the " +
|
|
146
|
-
`same verification options and that the JWKS endpoint at ${url
|
|
131
|
+
`same verification options and that the JWKS endpoint at ${joinURL(url, "/auth/keys")} is reachable.`);
|
|
147
132
|
return { isAuthenticated: false, session: null };
|
|
148
133
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
134
|
+
// A refused session, an anonymous one and one of another project read as
|
|
135
|
+
// signed out here. So does a check without a verdict, which
|
|
136
|
+
// `checkOpaqueSessionToken` logs with its cause.
|
|
137
|
+
const check = await checkOpaqueSession(token, url, opaqueTokenTimeoutMs, projectId);
|
|
138
|
+
if (check.verdict === "live" && check.session.userId) {
|
|
139
|
+
return {
|
|
140
|
+
isAuthenticated: true,
|
|
141
|
+
session: {
|
|
142
|
+
userId: check.session.userId,
|
|
143
|
+
identifier: check.session.identifier,
|
|
144
|
+
identifierProperty: check.session.identifierProperty,
|
|
145
|
+
display: check.session.display,
|
|
146
|
+
token,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
152
149
|
}
|
|
153
150
|
return { isAuthenticated: false, session: null };
|
|
154
151
|
}
|
|
152
|
+
let warnedMissingProject = false;
|
|
153
|
+
/** Says once per process why `auth()` signs no one in: no project is configured. */
|
|
154
|
+
function warnMissingProject() {
|
|
155
|
+
if (warnedMissingProject) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
warnedMissingProject = true;
|
|
159
|
+
console.warn("[nextgen] auth(): no project id is configured, so no session counts as signed in. " +
|
|
160
|
+
"Set ZITADEL_PROJECT_ID, or pass the middleware's `projectId` to auth() (with " +
|
|
161
|
+
"createProxy(config), that is config.projectId).");
|
|
162
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* for use in Next.js apps.
|
|
2
|
+
* Registers the `<zitadel-login>`, `<zitadel-logout>` and `<zitadel-session>`
|
|
3
|
+
* Lit web components for use in Next.js apps.
|
|
4
4
|
*
|
|
5
5
|
* Import this inside a `"use client"` boundary (e.g. a dynamic import
|
|
6
6
|
* with `{ ssr: false }`) to register the custom elements with the
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* await import("@zitadel/sdk-next/client");
|
|
11
11
|
* ```
|
|
12
12
|
*
|
|
13
|
+
* On an edge runtime the components package resolves to an entry without the
|
|
14
|
+
* elements, so this entry still loads there.
|
|
15
|
+
*
|
|
13
16
|
* SDK configuration is done via `configureZitadel()`, re-exported here so
|
|
14
17
|
* a consuming app that only declares `@zitadel/sdk-next` as a direct
|
|
15
18
|
* dependency can configure the SDK without reaching into
|
|
@@ -17,7 +20,8 @@
|
|
|
17
20
|
* resolve). Call it inside the same `"use client"` boundary before the
|
|
18
21
|
* components mount.
|
|
19
22
|
*/
|
|
20
|
-
|
|
23
|
+
import "@zitadel/components";
|
|
24
|
+
export { businessLocales } from "@zitadel/components";
|
|
21
25
|
export { configureZitadel, getApi } from "@zitadel/api/config";
|
|
22
26
|
export type { ZitadelConfig, ZitadelProject } from "@zitadel/api/config";
|
|
23
27
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,qBAAqB,CAAC;AAI7B,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* for use in Next.js apps.
|
|
2
|
+
* Registers the `<zitadel-login>`, `<zitadel-logout>` and `<zitadel-session>`
|
|
3
|
+
* Lit web components for use in Next.js apps.
|
|
4
4
|
*
|
|
5
5
|
* Import this inside a `"use client"` boundary (e.g. a dynamic import
|
|
6
6
|
* with `{ ssr: false }`) to register the custom elements with the
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* await import("@zitadel/sdk-next/client");
|
|
11
11
|
* ```
|
|
12
12
|
*
|
|
13
|
+
* On an edge runtime the components package resolves to an entry without the
|
|
14
|
+
* elements, so this entry still loads there.
|
|
15
|
+
*
|
|
13
16
|
* SDK configuration is done via `configureZitadel()`, re-exported here so
|
|
14
17
|
* a consuming app that only declares `@zitadel/sdk-next` as a direct
|
|
15
18
|
* dependency can configure the SDK without reaching into
|
|
@@ -17,5 +20,10 @@
|
|
|
17
20
|
* resolve). Call it inside the same `"use client"` boundary before the
|
|
18
21
|
* components mount.
|
|
19
22
|
*/
|
|
20
|
-
|
|
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";
|
|
21
29
|
export { configureZitadel, getApi } from "@zitadel/api/config";
|
package/dist/middleware.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ import { 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
|
* };
|
|
@@ -54,7 +57,7 @@ export 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: "
|
|
60
|
+
* projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
|
|
58
61
|
* url: process.env.ZITADEL_URL,
|
|
59
62
|
* });
|
|
60
63
|
*
|
|
@@ -71,6 +74,12 @@ export 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.
|
package/dist/middleware.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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"}
|
package/dist/middleware.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
2
3
|
import { NextResponse } from "next/server";
|
|
3
|
-
import { verifyJwt, isJwtShaped } from "./lib/jwt.js";
|
|
4
4
|
/**
|
|
5
5
|
* Clones the incoming request headers, injects `extra` key/value pairs,
|
|
6
6
|
* and registers them with the Next.js header-tunnelling mechanism so that
|
|
@@ -19,6 +19,8 @@ function tunnelHeaders(req, extra) {
|
|
|
19
19
|
// scratch using only the names we inject ourselves.
|
|
20
20
|
headers.delete("x-middleware-override-headers");
|
|
21
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.
|
|
22
24
|
for (const [name, value] of Object.entries(extra)) {
|
|
23
25
|
headers.set(name, value);
|
|
24
26
|
headers.set(`x-middleware-request-${name}`, value);
|
|
@@ -45,6 +47,9 @@ function tunnelHeaders(req, extra) {
|
|
|
45
47
|
* });
|
|
46
48
|
* }
|
|
47
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
|
+
*
|
|
48
53
|
* export const config = {
|
|
49
54
|
* matcher: ["/__nextgen/:path*", "/profile/:path*"],
|
|
50
55
|
* };
|
|
@@ -55,7 +60,7 @@ function tunnelHeaders(req, extra) {
|
|
|
55
60
|
* @returns A `NextResponse` or `Response` to continue, redirect, or proxy.
|
|
56
61
|
*/
|
|
57
62
|
export async function nextgenMiddleware(req, options = {}) {
|
|
58
|
-
const { url = process.env.ZITADEL_URL ?? "http://localhost:8080", proxyPath = "/__nextgen", protectedRoutes = [], ignoredRoutes = [], loginPath = "/login", allowedAlgorithms = ["RS256", "ES256"], clockSkewMs = 5000, audience, allowedTokenTypes = ["JWT", "at+JWT"], jwksTimeoutMs, proxyTimeoutMs = 5000, opaqueTokenTimeoutMs = 5000, } = 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;
|
|
59
64
|
// Guard against open-redirect: loginPath must be a relative path. An absolute
|
|
60
65
|
// URL (e.g. "https://evil.com/phish") or a protocol-relative URL
|
|
61
66
|
// (e.g. "//evil.com") would redirect the browser to an external host.
|
|
@@ -64,19 +69,21 @@ export async function nextgenMiddleware(req, options = {}) {
|
|
|
64
69
|
`Received: "${loginPath}". Using an absolute or protocol-relative URL ` +
|
|
65
70
|
`would allow open-redirect attacks.`);
|
|
66
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
|
+
}
|
|
67
77
|
const { pathname } = new URL(req.url);
|
|
68
78
|
if (matchesRoutes(pathname, ignoredRoutes)) {
|
|
69
|
-
|
|
70
|
-
// handleAuth is skipped for ignored routes, so we must strip the header
|
|
71
|
-
// here to prevent a forged value from reaching server components.
|
|
72
|
-
const headers = tunnelHeaders(req, { "x-nextgen-auth-token": "" });
|
|
73
|
-
return NextResponse.next({ request: { headers } });
|
|
79
|
+
return NextResponse.next();
|
|
74
80
|
}
|
|
75
81
|
if (pathname === proxyPath || pathname.startsWith(`${proxyPath}/`)) {
|
|
76
|
-
return proxyRequest(req, url, proxyPath, proxyTimeoutMs);
|
|
82
|
+
return proxyRequest(req, url, proxyPath, projectId, proxyTimeoutMs);
|
|
77
83
|
}
|
|
78
84
|
return handleAuth(req, {
|
|
79
85
|
url,
|
|
86
|
+
projectId,
|
|
80
87
|
protectedRoutes,
|
|
81
88
|
loginPath,
|
|
82
89
|
allowedAlgorithms,
|
|
@@ -88,30 +95,11 @@ export async function nextgenMiddleware(req, options = {}) {
|
|
|
88
95
|
pathname,
|
|
89
96
|
});
|
|
90
97
|
}
|
|
91
|
-
/**
|
|
92
|
-
* Validates an opaque (non-JWT) session token by calling the backend's
|
|
93
|
-
* `/sessions/me` endpoint. Returns `true` when the backend confirms the
|
|
94
|
-
* session is live (HTTP 200), `false` otherwise.
|
|
95
|
-
*
|
|
96
|
-
* This is the fallback path for backends that issue encrypted opaque tokens
|
|
97
|
-
* rather than self-contained JWTs.
|
|
98
|
-
*/
|
|
99
|
-
async function validateOpaqueSessionToken(token, issuerUrl, timeoutMs) {
|
|
100
|
-
try {
|
|
101
|
-
const res = await fetch(`${issuerUrl}/sessions/me`, {
|
|
102
|
-
method: "GET",
|
|
103
|
-
headers: { cookie: `__nextgen_session=${token}` },
|
|
104
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
105
|
-
});
|
|
106
|
-
return res.ok;
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
98
|
/**
|
|
113
99
|
* Forwards a `/__nextgen/*` request to the upstream auth backend and streams
|
|
114
|
-
* the response back
|
|
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}).
|
|
115
103
|
*
|
|
116
104
|
* All non-hop-by-hop headers from the incoming request are forwarded, including
|
|
117
105
|
* any `X-Forwarded-For` chain already set by an upstream CDN or load balancer.
|
|
@@ -123,12 +111,13 @@ async function validateOpaqueSessionToken(token, issuerUrl, timeoutMs) {
|
|
|
123
111
|
* @param req - The incoming edge request.
|
|
124
112
|
* @param authUrl - Base URL of the auth backend.
|
|
125
113
|
* @param proxyPath - The path prefix being proxied (e.g. `"/__nextgen"`).
|
|
114
|
+
* @param projectId - The project this app signs its users in to.
|
|
126
115
|
* @returns The proxied upstream `Response`.
|
|
127
116
|
*/
|
|
128
|
-
async function proxyRequest(req, authUrl, proxyPath, proxyTimeoutMs) {
|
|
117
|
+
async function proxyRequest(req, authUrl, proxyPath, projectId, proxyTimeoutMs) {
|
|
129
118
|
const url = new URL(req.url);
|
|
130
119
|
const suffix = url.pathname.slice(proxyPath.length);
|
|
131
|
-
const target = `${authUrl
|
|
120
|
+
const target = `${joinURL(authUrl, suffix)}${url.search}`;
|
|
132
121
|
const upstreamHeaders = new Headers();
|
|
133
122
|
for (const [key, value] of req.headers.entries()) {
|
|
134
123
|
const lower = key.toLowerCase();
|
|
@@ -140,7 +129,7 @@ async function proxyRequest(req, authUrl, proxyPath, proxyTimeoutMs) {
|
|
|
140
129
|
// upstream auth server sees the full proxy path. We never skip this even when
|
|
141
130
|
// a chain is already present — a CDN or load balancer may have set XFF before
|
|
142
131
|
// the request reached this edge node, and our hop must still be recorded.
|
|
143
|
-
const directIp = req.ip
|
|
132
|
+
const directIp = req.ip;
|
|
144
133
|
if (directIp) {
|
|
145
134
|
const existingXff = upstreamHeaders.get("x-forwarded-for");
|
|
146
135
|
upstreamHeaders.set("x-forwarded-for", existingXff ? `${existingXff}, ${directIp}` : directIp);
|
|
@@ -151,6 +140,21 @@ async function proxyRequest(req, authUrl, proxyPath, proxyTimeoutMs) {
|
|
|
151
140
|
if (!upstreamHeaders.has("x-forwarded-proto")) {
|
|
152
141
|
upstreamHeaders.set("x-forwarded-proto", url.protocol.replace(":", ""));
|
|
153
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
|
+
}
|
|
154
158
|
const hasBody = !["GET", "HEAD"].includes(req.method);
|
|
155
159
|
// ADR 036: the current browser flow needs the confidential project secret
|
|
156
160
|
// only for the handoff exchange. Stamping it onto every proxied operation
|
|
@@ -177,80 +181,77 @@ async function proxyRequest(req, authUrl, proxyPath, proxyTimeoutMs) {
|
|
|
177
181
|
// The app's own request URL: the origin a proxied redirect may name, and
|
|
178
182
|
// the base a relative one resolves against.
|
|
179
183
|
const responseHeaders = filterResponseHeaders(upstream.headers, req.nextUrl.toString());
|
|
180
|
-
const
|
|
181
|
-
for (const cookie of setCookies) {
|
|
184
|
+
const answer = await sessionCookies.toBrowser(upstream);
|
|
185
|
+
for (const cookie of answer.setCookies) {
|
|
182
186
|
responseHeaders.append("set-cookie", cookie);
|
|
183
187
|
}
|
|
184
|
-
|
|
188
|
+
if (answer.setCookies.length > 0) {
|
|
189
|
+
responseHeaders.set("cache-control", SESSION_COOKIE_CACHE_CONTROL);
|
|
190
|
+
}
|
|
191
|
+
return new Response(answer.body, {
|
|
185
192
|
status: upstream.status,
|
|
186
193
|
headers: responseHeaders,
|
|
187
194
|
});
|
|
188
195
|
}
|
|
189
|
-
function requiresProjectSecret(method, pathname) {
|
|
190
|
-
return method.toUpperCase() === "POST" && pathname === "/sessions/exchange";
|
|
191
|
-
}
|
|
192
196
|
/**
|
|
193
|
-
*
|
|
194
|
-
* a request header
|
|
195
|
-
*
|
|
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.
|
|
196
203
|
*
|
|
197
204
|
* @param req - The incoming edge request.
|
|
198
205
|
* @param opts - Auth handler options.
|
|
199
|
-
* @returns A `NextResponse` to continue or
|
|
206
|
+
* @returns A `NextResponse` to continue, redirect, or answer `503`.
|
|
200
207
|
*/
|
|
201
208
|
async function handleAuth(req, opts) {
|
|
202
|
-
const {
|
|
209
|
+
const { protectedRoutes, loginPath, pathname, ...verification } = opts;
|
|
203
210
|
const authHeader = req.headers.get("authorization");
|
|
204
211
|
const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
allowedTokenTypes,
|
|
217
|
-
jwksTimeoutMs,
|
|
218
|
-
})
|
|
219
|
-
: null;
|
|
220
|
-
if (payload && token && payload.sub) {
|
|
221
|
-
const tunnelled = tunnelHeaders(req, { "x-nextgen-auth-token": token });
|
|
222
|
-
return NextResponse.next({ request: { headers: tunnelled } });
|
|
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
223
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (!payload && cookieToken && !isJwtShaped(cookieToken)) {
|
|
230
|
-
const isValid = await validateOpaqueSessionToken(cookieToken, url, opaqueTokenTimeoutMs);
|
|
231
|
-
if (isValid) {
|
|
232
|
-
const tunnelled = tunnelHeaders(req, {
|
|
233
|
-
"x-nextgen-auth-token": cookieToken,
|
|
234
|
-
});
|
|
235
|
-
return NextResponse.next({ request: { headers: tunnelled } });
|
|
236
|
-
}
|
|
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 });
|
|
237
229
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
loginUrl.searchParams.set("next", pathname);
|
|
245
|
-
const redirect = NextResponse.redirect(loginUrl, { status: 302 });
|
|
246
|
-
for (const cookie of staleNextgenCookies) {
|
|
247
|
-
redirect.cookies.delete(cookie.name);
|
|
248
|
-
}
|
|
249
|
-
return redirect;
|
|
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 } });
|
|
250
236
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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);
|
|
254
255
|
}
|
|
255
256
|
return response;
|
|
256
257
|
}
|
|
@@ -265,7 +266,7 @@ async function handleAuth(req, opts) {
|
|
|
265
266
|
* import { createProxy } from "@zitadel/sdk-next/middleware";
|
|
266
267
|
*
|
|
267
268
|
* const zitadel = configureZitadel({
|
|
268
|
-
* projectId: "
|
|
269
|
+
* projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
|
|
269
270
|
* url: process.env.ZITADEL_URL,
|
|
270
271
|
* });
|
|
271
272
|
*
|
|
@@ -282,6 +283,12 @@ async function handleAuth(req, opts) {
|
|
|
282
283
|
* export const middleware = proxy;
|
|
283
284
|
* ```
|
|
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
|
+
*
|
|
285
292
|
* @param config - The SDK handle from `configureZitadel()`.
|
|
286
293
|
* @param options - Route protection and JWT options.
|
|
287
294
|
* @returns A middleware handler function.
|
|
@@ -289,6 +296,7 @@ async function handleAuth(req, opts) {
|
|
|
289
296
|
export function createProxy(config, options = {}) {
|
|
290
297
|
const mergedOptions = {
|
|
291
298
|
...options,
|
|
299
|
+
projectId: options.projectId ?? (config.projectId || undefined),
|
|
292
300
|
proxyPath: config.proxyPath,
|
|
293
301
|
url: config.url,
|
|
294
302
|
};
|
package/dist/server.d.ts
CHANGED
|
@@ -9,4 +9,5 @@
|
|
|
9
9
|
export { auth } from "./auth.js";
|
|
10
10
|
export type { AuthOptions } from "./auth.js";
|
|
11
11
|
export { NextgenProvider } from "./provider.js";
|
|
12
|
+
export type { AuthResult, AuthState, UnauthState, NextgenSession, ClientAuthResult, NextgenMiddlewareOptions, } from "./types.js";
|
|
12
13
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,YAAY,EACV,UAAU,EACV,SAAS,EACT,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,YAAY,CAAC"}
|
package/dist/session.d.ts
CHANGED
|
@@ -7,24 +7,37 @@ export type GetSessionOptions = {
|
|
|
7
7
|
* on this page, else `"/__nextgen"` (the scaffold default).
|
|
8
8
|
*/
|
|
9
9
|
proxyPath?: string;
|
|
10
|
+
/**
|
|
11
|
+
* The project this app signs its users in to. A session of another project
|
|
12
|
+
* reads as signed out. Defaults to the project from `configureZitadel()`
|
|
13
|
+
* when it has run on this page, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`. With
|
|
14
|
+
* none, nobody reads as signed in, as on the server.
|
|
15
|
+
*/
|
|
16
|
+
projectId?: string;
|
|
10
17
|
};
|
|
11
18
|
/**
|
|
12
19
|
* Reads the current session state from the browser — the supported way for
|
|
13
20
|
* an app's own UI (header navigation, account menus) to know whether a user
|
|
14
21
|
* is signed in and who they are.
|
|
15
22
|
*
|
|
16
|
-
* Fetches the same-origin `{proxyPath}/sessions/me
|
|
17
|
-
* `cache: "no-store"` — the same read the `<zitadel-session>`
|
|
18
|
-
* so the answer is the server's, not a client-side guess.
|
|
19
|
-
* unlike server-side `auth()`, it does not require the
|
|
20
|
-
* the middleware `matcher` (only the proxy path itself
|
|
21
|
-
* the scaffolded request boundary always does)
|
|
22
|
-
*
|
|
23
|
+
* Fetches the same-origin `{proxyPath}/sessions/me?project_id=…` with
|
|
24
|
+
* credentials and `cache: "no-store"` — the same read the `<zitadel-session>`
|
|
25
|
+
* card performs — so the answer is the server's, not a client-side guess.
|
|
26
|
+
* Works on any page: unlike server-side `auth()`, it does not require the
|
|
27
|
+
* route to be covered by the middleware `matcher` (only the proxy path itself
|
|
28
|
+
* must be matched, which the scaffolded request boundary always does).
|
|
29
|
+
*
|
|
30
|
+
* A session counts only in the app's own project: `options.projectId`, else
|
|
31
|
+
* the project of `configureZitadel()`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`.
|
|
32
|
+
* With none of them, it reads as signed out without asking the server.
|
|
23
33
|
*
|
|
24
34
|
* - `200` with an authenticated user → `{ isAuthenticated: true, session }`
|
|
25
35
|
* with the client-safe identity (`userId`, `email`, `name` — no token).
|
|
26
|
-
* - `200` for an anonymous session, `401` with `auth.unauthorized`,
|
|
27
|
-
* with `sess.not_found` → signed out.
|
|
36
|
+
* - `200` for an anonymous session, `401` with `auth.unauthorized`, `403`
|
|
37
|
+
* with `sess.project_mismatch`, or `404` with `sess.not_found` → signed out.
|
|
38
|
+
* The `403` answers a session of another project on the same server, and a
|
|
39
|
+
* `200` naming another project (from a server that predates the parameter)
|
|
40
|
+
* reads as signed out too.
|
|
28
41
|
* - Any other response throws — including a framework's HTML 404 page from
|
|
29
42
|
* a misrouted proxy: a failing proxy is a misconfiguration and must not
|
|
30
43
|
* silently render as "signed out". Treat a rejection as "unknown" —
|
|
@@ -39,7 +52,10 @@ export type GetSessionOptions = {
|
|
|
39
52
|
* const [auth, setAuth] = useState<ClientAuthResult>();
|
|
40
53
|
* const [failed, setFailed] = useState(false);
|
|
41
54
|
* useEffect(() => {
|
|
42
|
-
* getSession().then(
|
|
55
|
+
* getSession({ projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID }).then(
|
|
56
|
+
* setAuth,
|
|
57
|
+
* () => setFailed(true),
|
|
58
|
+
* );
|
|
43
59
|
* }, []);
|
|
44
60
|
* if (failed) return <span role="alert">Session unavailable</span>;
|
|
45
61
|
* if (!auth) return null;
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKnD,sCAAsC;AACtC,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKnD,sCAAsC;AACtC,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AA8BF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,wBAAsB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAqE3F;AAED,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/session.js
CHANGED
|
@@ -1,23 +1,53 @@
|
|
|
1
1
|
import { getZitadelConfig } from "@zitadel/api/config";
|
|
2
2
|
/** Matches the `configureZitadel()` default so zero-config apps agree with the scaffold. */
|
|
3
3
|
const DEFAULT_PROXY_PATH = "/__nextgen";
|
|
4
|
+
/**
|
|
5
|
+
* `NEXT_PUBLIC_ZITADEL_PROJECT_ID`, which Next.js inlines into client bundles
|
|
6
|
+
* at build time. Read defensively: where it was not inlined, `process` may not
|
|
7
|
+
* exist in the browser.
|
|
8
|
+
*/
|
|
9
|
+
function publicProjectId() {
|
|
10
|
+
try {
|
|
11
|
+
return process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID || undefined;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
let warnedMissingProject = false;
|
|
18
|
+
/** Says once per page why `getSession()` signs no one in: no project is known. */
|
|
19
|
+
function warnMissingProject() {
|
|
20
|
+
if (warnedMissingProject) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
warnedMissingProject = true;
|
|
24
|
+
console.warn("[nextgen] getSession(): no project id is known, so no session counts as signed in. " +
|
|
25
|
+
"Pass `projectId` (e.g. process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID) or call " +
|
|
26
|
+
"configureZitadel() first.");
|
|
27
|
+
}
|
|
4
28
|
/**
|
|
5
29
|
* Reads the current session state from the browser — the supported way for
|
|
6
30
|
* an app's own UI (header navigation, account menus) to know whether a user
|
|
7
31
|
* is signed in and who they are.
|
|
8
32
|
*
|
|
9
|
-
* Fetches the same-origin `{proxyPath}/sessions/me
|
|
10
|
-
* `cache: "no-store"` — the same read the `<zitadel-session>`
|
|
11
|
-
* so the answer is the server's, not a client-side guess.
|
|
12
|
-
* unlike server-side `auth()`, it does not require the
|
|
13
|
-
* the middleware `matcher` (only the proxy path itself
|
|
14
|
-
* the scaffolded request boundary always does)
|
|
15
|
-
*
|
|
33
|
+
* Fetches the same-origin `{proxyPath}/sessions/me?project_id=…` with
|
|
34
|
+
* credentials and `cache: "no-store"` — the same read the `<zitadel-session>`
|
|
35
|
+
* card performs — so the answer is the server's, not a client-side guess.
|
|
36
|
+
* Works on any page: unlike server-side `auth()`, it does not require the
|
|
37
|
+
* route to be covered by the middleware `matcher` (only the proxy path itself
|
|
38
|
+
* must be matched, which the scaffolded request boundary always does).
|
|
39
|
+
*
|
|
40
|
+
* A session counts only in the app's own project: `options.projectId`, else
|
|
41
|
+
* the project of `configureZitadel()`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`.
|
|
42
|
+
* With none of them, it reads as signed out without asking the server.
|
|
16
43
|
*
|
|
17
44
|
* - `200` with an authenticated user → `{ isAuthenticated: true, session }`
|
|
18
45
|
* with the client-safe identity (`userId`, `email`, `name` — no token).
|
|
19
|
-
* - `200` for an anonymous session, `401` with `auth.unauthorized`,
|
|
20
|
-
* with `sess.not_found` → signed out.
|
|
46
|
+
* - `200` for an anonymous session, `401` with `auth.unauthorized`, `403`
|
|
47
|
+
* with `sess.project_mismatch`, or `404` with `sess.not_found` → signed out.
|
|
48
|
+
* The `403` answers a session of another project on the same server, and a
|
|
49
|
+
* `200` naming another project (from a server that predates the parameter)
|
|
50
|
+
* reads as signed out too.
|
|
21
51
|
* - Any other response throws — including a framework's HTML 404 page from
|
|
22
52
|
* a misrouted proxy: a failing proxy is a misconfiguration and must not
|
|
23
53
|
* silently render as "signed out". Treat a rejection as "unknown" —
|
|
@@ -32,7 +62,10 @@ const DEFAULT_PROXY_PATH = "/__nextgen";
|
|
|
32
62
|
* const [auth, setAuth] = useState<ClientAuthResult>();
|
|
33
63
|
* const [failed, setFailed] = useState(false);
|
|
34
64
|
* useEffect(() => {
|
|
35
|
-
* getSession().then(
|
|
65
|
+
* getSession({ projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID }).then(
|
|
66
|
+
* setAuth,
|
|
67
|
+
* () => setFailed(true),
|
|
68
|
+
* );
|
|
36
69
|
* }, []);
|
|
37
70
|
* if (failed) return <span role="alert">Session unavailable</span>;
|
|
38
71
|
* if (!auth) return null;
|
|
@@ -62,7 +95,12 @@ export async function getSession(options = {}) {
|
|
|
62
95
|
while (proxyPath.endsWith("/")) {
|
|
63
96
|
proxyPath = proxyPath.slice(0, -1);
|
|
64
97
|
}
|
|
65
|
-
const
|
|
98
|
+
const projectId = options.projectId || getZitadelConfig()?.projectId || publicProjectId();
|
|
99
|
+
if (!projectId) {
|
|
100
|
+
warnMissingProject();
|
|
101
|
+
return { isAuthenticated: false, session: null };
|
|
102
|
+
}
|
|
103
|
+
const response = await fetch(`${proxyPath}/sessions/me?project_id=${encodeURIComponent(projectId)}`, {
|
|
66
104
|
cache: "no-store",
|
|
67
105
|
credentials: "include",
|
|
68
106
|
headers: { accept: "application/json" },
|
|
@@ -70,9 +108,10 @@ export async function getSession(options = {}) {
|
|
|
70
108
|
// Only the backend's canonical error envelope is definitive signed-out.
|
|
71
109
|
// A framework route, gateway, or WAF can also return 401/404; accepting the
|
|
72
110
|
// status or content type alone would turn a broken proxy into signed-out.
|
|
73
|
-
if (response.status === 401 || response.status === 404) {
|
|
111
|
+
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
74
112
|
const error = await readErrorEnvelope(response);
|
|
75
113
|
const isSignedOut = (response.status === 401 && error?.code === "auth.unauthorized") ||
|
|
114
|
+
(response.status === 403 && error?.code === "sess.project_mismatch") ||
|
|
76
115
|
(response.status === 404 && error?.code === "sess.not_found");
|
|
77
116
|
if (isSignedOut) {
|
|
78
117
|
return { isAuthenticated: false, session: null };
|
|
@@ -82,9 +121,10 @@ export async function getSession(options = {}) {
|
|
|
82
121
|
throw new Error(`[nextgen] Session read failed: HTTP ${response.status} from ${proxyPath}/sessions/me`);
|
|
83
122
|
}
|
|
84
123
|
const session = (await response.json());
|
|
85
|
-
// An anonymous session (no verified user factor yet) has no user_id
|
|
86
|
-
//
|
|
87
|
-
|
|
124
|
+
// An anonymous session (no verified user factor yet) has no user_id, and a
|
|
125
|
+
// backend that predates `project_id` answers for any project: for app
|
|
126
|
+
// chrome, either is "not signed in".
|
|
127
|
+
if (!session.user_id || session.project_id !== projectId) {
|
|
88
128
|
return { isAuthenticated: false, session: null };
|
|
89
129
|
}
|
|
90
130
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zitadel/sdk-next",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.28",
|
|
4
4
|
"description": "Next.js helpers and mock auth UI for Zitadel",
|
|
5
5
|
"homepage": "https://github.com/zitadel/nextgen/tree/main/packages/sdk-next#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -54,46 +54,37 @@
|
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
+
"@zitadel/api": "1.0.0-alpha.28",
|
|
58
|
+
"@zitadel/components": "1.0.0-alpha.28",
|
|
59
|
+
"@zitadel/sdk-core": "1.0.0-alpha.28",
|
|
57
60
|
"server-only": "^0.0.1",
|
|
58
|
-
"
|
|
59
|
-
"@zitadel/sdk-core": "1.0.0-alpha.24",
|
|
60
|
-
"@zitadel/components": "1.0.0-alpha.24"
|
|
61
|
+
"ufo": "^1.6.4"
|
|
61
62
|
},
|
|
62
63
|
"peerDependencies": {
|
|
63
|
-
"next": ">=15",
|
|
64
|
+
"next": ">=15.2.3",
|
|
64
65
|
"react": ">=18",
|
|
65
66
|
"react-dom": ">=18"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
|
-
"@
|
|
69
|
-
"@eslint/json": "^0.11.0",
|
|
70
|
-
"@eslint/markdown": "^6.0.0",
|
|
69
|
+
"@edge-runtime/vm": "^5.0.0",
|
|
71
70
|
"@testing-library/react": "^16.3.2",
|
|
72
71
|
"@testing-library/user-event": "^14.6.1",
|
|
72
|
+
"@types/node": "^24.19.1",
|
|
73
73
|
"@types/react": "^19.2.14",
|
|
74
|
-
"@vitest/coverage-v8": "^
|
|
75
|
-
"eslint": "^9.0.0",
|
|
76
|
-
"eslint-config-prettier": "^10.0.0",
|
|
77
|
-
"eslint-import-resolver-typescript": "^3.7.0",
|
|
78
|
-
"eslint-plugin-import": "^2.31.0",
|
|
79
|
-
"eslint-plugin-jsx-a11y": "^6.10.0",
|
|
80
|
-
"eslint-plugin-perfectionist": "^4.0.0",
|
|
81
|
-
"eslint-plugin-prettier": "^5.0.0",
|
|
82
|
-
"eslint-plugin-react": "^7.37.0",
|
|
83
|
-
"eslint-plugin-react-hooks": "^5.0.0",
|
|
84
|
-
"eslint-plugin-testing-library": "^7.0.0",
|
|
74
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
85
75
|
"jsdom": "^29.0.2",
|
|
86
|
-
"next": "^16.
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"typescript
|
|
90
|
-
"vitest": "^
|
|
76
|
+
"next": "^16.3.8",
|
|
77
|
+
"react": "^19.2.8",
|
|
78
|
+
"react-dom": "^19.2.8",
|
|
79
|
+
"typescript": "^6.0.3",
|
|
80
|
+
"vitest": "^4.1.11"
|
|
91
81
|
},
|
|
92
82
|
"scripts": {
|
|
93
83
|
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json && node -e \"require('node:fs').copyFileSync('src/jsx.d.ts', 'dist/jsx.d.ts')\"",
|
|
94
|
-
"typecheck": "tsc --
|
|
84
|
+
"typecheck": "tsc --noEmit -p tsconfig.lib.json && tsc --noEmit -p tsconfig.spec.json",
|
|
95
85
|
"test": "vitest run",
|
|
96
86
|
"test:all": "pnpm run test",
|
|
97
|
-
"lint": "
|
|
87
|
+
"lint": "biome check .",
|
|
88
|
+
"build-release": "node ../../scripts/release-clean.mjs local-dist && pnpm run build"
|
|
98
89
|
}
|
|
99
90
|
}
|