@zitadel/sdk-nuxt 0.1.0-alpha.17 → 0.1.0-alpha.19

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @zitadel/sdk-nuxt
2
2
 
3
- Nuxt middleware and helpers for Nextgen Auth.
3
+ Nuxt module, Nitro middleware, and helpers for Nextgen Auth.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,67 +8,66 @@ Nuxt middleware and helpers for Nextgen Auth.
8
8
  pnpm add @zitadel/sdk-nuxt
9
9
  ```
10
10
 
11
- ## Setup
11
+ There are two ways to wire the SDK. **The module is what `zitadel setup`
12
+ scaffolds** and the recommended path; the direct-middleware surface remains
13
+ for hand-rolled setups.
12
14
 
13
- ### 1. Server middleware
15
+ ## Surface 1 the Nuxt module (recommended, CLI-scaffolded)
14
16
 
15
- Create `server/middleware/auth.ts`:
16
-
17
- ```ts
18
- import { createNextgenMiddleware } from '@zitadel/sdk-nuxt/server';
19
-
20
- const { nextgen } = useRuntimeConfig();
21
-
22
- export default createNextgenMiddleware({
23
- url: nextgen.url,
24
- protectedRoutes: ['/admin', '/dashboard*'],
25
- loginPath: '/login',
26
- });
27
- ```
28
-
29
- The middleware runs on every request and does three things in one pass:
30
-
31
- 1. **Proxies** `/__nextgen/*` requests to the auth backend
32
- 2. **Verifies** the session JWT via JWKS using the Web Crypto API
33
- 3. **Redirects** unauthenticated requests to `loginPath` for protected routes
34
-
35
- Add the issuer URL to `nuxt.config.ts`:
17
+ Register the module under the `nextgen` config key, and give the client
18
+ plugin your project id via `runtimeConfig.public.zitadelProjectId` — without
19
+ it the plugin skips `configureZitadel()`, `useZitadelProject()` returns
20
+ `null`, and the login widget cannot initialize:
36
21
 
37
22
  ```ts
38
23
  export default defineNuxtConfig({
24
+ modules: ['@zitadel/sdk-nuxt/module'],
25
+ nextgen: {
26
+ url: process.env.ZITADEL_URL ?? 'http://localhost:8080',
27
+ protectedRoutes: ['/admin', '/dashboard*'],
28
+ loginPath: '/login',
29
+ },
39
30
  runtimeConfig: {
40
- nextgen: {
41
- url: process.env.ZITADEL_URL ?? 'http://localhost:4000',
31
+ public: {
32
+ zitadelProjectId: process.env.NUXT_PUBLIC_ZITADEL_PROJECT_ID ?? '',
42
33
  },
43
34
  },
44
35
  });
45
36
  ```
46
37
 
47
- ### 2. Plugin
38
+ (This mirrors what `zitadel setup` writes into `nuxt.config.ts`.)
48
39
 
49
- Create `plugins/auth.server.ts` to make auth state available in pages:
40
+ The module registers the Nitro server middleware, the auth plugin (which
41
+ seeds server-side auth state, hydrates it on the client, and calls
42
+ `configureZitadel()` from the runtime config above), and auto-imports the
43
+ composables — no `server/middleware/auth.ts`, no manual plugin.
50
44
 
51
- ```ts
52
- import { defineNuxtPlugin, useRequestEvent, useState } from '#imports';
53
-
54
- export default defineNuxtPlugin(() => {
55
- const event = useRequestEvent();
56
- const auth = event?.context.nextgenAuth ?? {
57
- isAuthenticated: false as const,
58
- session: null,
59
- };
60
- useState('nextgen-auth', () => auth);
61
- });
62
- ```
45
+ ### Module options (what the module actually forwards)
46
+
47
+ | Option | Type | Default | Description |
48
+ | --- | --- | --- | --- |
49
+ | `url` | `string` | `ZITADEL_URL` env, else `http://localhost:8080` | Full URL of the Zitadel auth backend |
50
+ | `proxyPath` | `string` | `"/__nextgen"` | The path the **client** widgets call (exposed via public runtime config). The registered server handler currently always serves `/__nextgen` regardless of this option — to actually move the server prefix, use the direct-middleware surface |
51
+ | `protectedRoutes` | `string[]` | `[]` | Paths requiring a valid session. Trailing `*` matches sub-paths |
52
+ | `loginPath` | `string` | `"/login"` | Where to redirect unauthenticated users |
63
53
 
64
- ### 3. Reading auth in a page
54
+ The module's registered handler reads only `url`, `protectedRoutes`, and
55
+ `loginPath` from runtime config; `proxyPath` configures the client side only
56
+ (see its row above). The fine-tuning options of the direct-middleware surface
57
+ (`ignoredRoutes`, `allowedAlgorithms`, `allowedTokenTypes`, `clockSkewMs`,
58
+ `jwksTimeoutMs`, `opaqueTokenTimeoutMs`, `proxyTimeoutMs`, `audience`) are
59
+ **not currently forwarded by the module** — if you need them, use Surface 2.
60
+ The module also loads the server-only project secret into runtime config at
61
+ startup, from the `ZITADEL_PROJECT_SECRET` environment variable or the
62
+ scaffolded `.env.local`.
63
+
64
+ ### Reading auth state
65
+
66
+ `useAuth()` is auto-imported:
65
67
 
66
68
  ```vue
67
69
  <script setup lang="ts">
68
- const auth = useState('nextgen-auth');
69
- if (auth.value?.isAuthenticated) {
70
- await navigateTo('/admin');
71
- }
70
+ const auth = useAuth();
72
71
  </script>
73
72
 
74
73
  <template>
@@ -76,10 +75,30 @@ if (auth.value?.isAuthenticated) {
76
75
  </template>
77
76
  ```
78
77
 
79
- ### 4. Register components (client only)
78
+ The returned state intentionally omits the raw JWT — use `getAuth(event)` in
79
+ a server route when you need the token to call upstream APIs:
80
+
81
+ ```ts
82
+ import { getAuth } from '@zitadel/sdk-nuxt/server';
83
+
84
+ export default defineEventHandler((event) => {
85
+ const auth = getAuth(event);
86
+ if (!auth.isAuthenticated) throw createError({ statusCode: 401 });
87
+ return { userId: auth.session.userId };
88
+ });
89
+ ```
90
+
91
+ ### Login page
92
+
93
+ Register the shared components in a client-only plugin
94
+ (`plugins/zitadel-components.client.ts` — do **not** import
95
+ `@zitadel/components` from page `<script setup>`, that runs during SSR).
96
+ Under strict package managers (pnpm, Yarn PnP) `@zitadel/components` is not
97
+ resolvable as a transitive dependency, so declare it in your app first:
80
98
 
81
- Create `plugins/zitadel-components.client.ts` — do **not** import
82
- `@zitadel/components` from page `<script setup>` (that runs during SSR):
99
+ ```bash
100
+ pnpm add @zitadel/components
101
+ ```
83
102
 
84
103
  ```ts
85
104
  import "@zitadel/components";
@@ -87,37 +106,48 @@ import "@zitadel/components";
87
106
  export default defineNuxtPlugin(() => {});
88
107
  ```
89
108
 
90
- Set `body { margin: 0; font-family: sans-serif; }` in `app.vue` (see
91
- [`apps/demo-nuxt`](../../apps/demo-nuxt/README.md)). Arimo loads from
92
- `branding.font_url` inside `<zitadel-login>` when the mock/API supplies it.
93
-
94
- ### 5. Login page
95
-
96
- Render `<zitadel-login>` inside `<ClientOnly>`:
109
+ Then render `<zitadel-login>` inside `<ClientOnly>`, binding the project
110
+ handle from the auto-imported `useZitadelProject()` composable there is no
111
+ `api-base` attribute:
97
112
 
98
113
  ```vue
114
+ <script setup lang="ts">
115
+ const project = useZitadelProject();
116
+ </script>
117
+
99
118
  <template>
100
119
  <main>
101
120
  <ClientOnly>
102
- <zitadel-login api-base="/__nextgen" project-id="demo" post-sign-in-url="/admin" />
121
+ <zitadel-login :project="project" post-sign-in-url="/admin" />
103
122
  </ClientOnly>
104
123
  </main>
105
124
  </template>
106
125
  ```
107
126
 
108
- ### 6. Reading auth in a server route
127
+ ## Surface 2 direct middleware (hand-rolled)
128
+
129
+ Create `server/middleware/auth.ts` yourself when you need the full option
130
+ set:
109
131
 
110
132
  ```ts
111
- import { getAuth } from '@zitadel/sdk-nuxt/server';
133
+ import { createNextgenMiddleware } from '@zitadel/sdk-nuxt/server';
112
134
 
113
- export default defineEventHandler((event) => {
114
- const auth = getAuth(event);
115
- if (!auth.isAuthenticated) throw createError({ statusCode: 401 });
116
- return { userId: auth.session.userId };
135
+ const { nextgen } = useRuntimeConfig();
136
+
137
+ export default createNextgenMiddleware({
138
+ url: nextgen.url,
139
+ protectedRoutes: ['/admin', '/dashboard*'],
140
+ loginPath: '/login',
117
141
  });
118
142
  ```
119
143
 
120
- ## Middleware options
144
+ The middleware runs on every request and does three things in one pass:
145
+
146
+ 1. **Proxies** `/__nextgen/*` requests to the auth backend
147
+ 2. **Verifies** the session JWT via JWKS using the Web Crypto API
148
+ 3. **Redirects** unauthenticated requests to `loginPath` for protected routes
149
+
150
+ ### Direct-middleware options
121
151
 
122
152
  | Option | Type | Default | Description |
123
153
  | ------------------- | -------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
@@ -130,17 +160,13 @@ export default defineEventHandler((event) => {
130
160
  | `allowedTokenTypes` | `string[]` | `["JWT", "at+JWT"]` | Accepted `typ` header values (case-insensitive). Set to `[]` to disable this check |
131
161
  | `clockSkewMs` | `number` | `5000` | Clock skew tolerance in ms for `exp`, `nbf`, `iat` |
132
162
  | `jwksTimeoutMs` | `number` | `5000` | Timeout in ms for JWKS endpoint requests. Token is rejected if the fetch exceeds this window |
163
+ | `opaqueTokenTimeoutMs` | `number` | `5000` | Timeout in ms for opaque (non-JWT) session validation via `GET /sessions/me` |
164
+ | `proxyTimeoutMs` | `number` | `5000` | Timeout in ms for upstream proxy requests; requests exceeding it abort with a network error |
133
165
  | `audience` | `string \| string[]` | not validated | Expected `aud` claim value(s). When omitted, audience is not checked |
134
166
 
135
167
  ## How JWT verification works
136
168
 
137
- 1. Bearer token from `Authorization` header is checked first; `__nextgen_session` cookie is the fallback
138
- 2. The JWT header is decoded to extract `kid` and `alg`
139
- 3. Tokens with an `alg` not in `allowedAlgorithms` (`RS256`, `ES256` by default) are rejected immediately — no JWKS fetch
140
- 4. Tokens with a `typ` not in `allowedTokenTypes` are rejected immediately
141
- 5. The public key is fetched from `{url}/auth/keys` (JWKS) using the Web Crypto API, with a 5 s timeout, and cached for 5 minutes per `kid`
142
- 6. The signature is verified **before** any claim checks
143
- 7. `iss` must be present and must equal `url` — tokens without an issuer are rejected
144
- 8. `exp` must be present and must be in the future (with `clockSkewMs` tolerance) — tokens without an expiry are rejected
145
- 9. `nbf` and `iat` are validated with `clockSkewMs` tolerance when present
146
- 10. The `x-nextgen-auth-token` header is stripped from all proxied requests to prevent internal state leakage
169
+ The verification pipeline is shared across SDKs and documented once in
170
+ [`@zitadel/sdk-core`](https://github.com/zitadel/nextgen/tree/main/packages/sdk-core#how-jwt-verification-works).
171
+ On top of it, the Nitro middleware strips the `x-nextgen-auth-token` header
172
+ from all proxied requests to prevent internal state leakage.
@@ -96,7 +96,7 @@ async function proxyRequest(event, authUrl, proxyPath, url, proxyTimeoutMs) {
96
96
  const rawBody = hasBody ? await readRawBody(event, false) : void 0;
97
97
  const body = rawBody != null ? new Uint8Array(rawBody) : void 0;
98
98
  const upstreamHeaders = buildUpstreamHeaders(event);
99
- if (!upstreamHeaders.has("authorization")) {
99
+ if (requiresProjectSecret(method, suffix) && !upstreamHeaders.has("authorization")) {
100
100
  const config = useRuntimeConfig();
101
101
  const projectSecret = config.nextgen?.projectSecret;
102
102
  if (projectSecret) {
@@ -130,6 +130,9 @@ async function proxyRequest(event, authUrl, proxyPath, url, proxyTimeoutMs) {
130
130
  }
131
131
  return response.body;
132
132
  }
133
+ function requiresProjectSecret(method, pathname) {
134
+ return method.toUpperCase() === "POST" && pathname === "/sessions/exchange";
135
+ }
133
136
  var DECODER = new TextDecoder();
134
137
  function isJwtShaped(token) {
135
138
  const parts = token.split(".");
@@ -193,13 +196,14 @@ async function handleAuth(event, opts) {
193
196
  };
194
197
  return;
195
198
  }
199
+ let liveAnonymousSession = false;
196
200
  if (!payload && cookieToken && !isJwtShaped(cookieToken)) {
197
201
  const opaqueResult = await validateOpaqueSessionToken(cookieToken, url, opaqueTokenTimeoutMs);
198
- if (opaqueResult) {
202
+ if (opaqueResult?.userId) {
199
203
  event.context.nextgenAuth = {
200
204
  isAuthenticated: true,
201
205
  session: {
202
- userId: opaqueResult.userId ?? "unknown",
206
+ userId: opaqueResult.userId,
203
207
  email: null,
204
208
  name: null,
205
209
  token: cookieToken
@@ -207,11 +211,14 @@ async function handleAuth(event, opts) {
207
211
  };
208
212
  return;
209
213
  }
214
+ liveAnonymousSession = opaqueResult !== null;
210
215
  }
211
216
  event.context.nextgenAuth = { isAuthenticated: false, session: null };
212
- for (const name of Object.keys(parseCookies(event))) {
213
- if (name.startsWith("__nextgen")) {
214
- deleteCookie(event, name);
217
+ if (!liveAnonymousSession) {
218
+ for (const name of Object.keys(parseCookies(event))) {
219
+ if (name.startsWith("__nextgen")) {
220
+ deleteCookie(event, name);
221
+ }
215
222
  }
216
223
  }
217
224
  if (matchesRoutes(pathname, protectedRoutes)) {
package/dist/index.d.ts CHANGED
@@ -1,8 +1,7 @@
1
- export { ZitadelLogin, ZitadelLogout } from '@zitadel/components';
1
+ export { ZitadelLogin, ZitadelLogout, businessLocales } from '@zitadel/components';
2
2
  export { createNextgenMiddleware, getAuth } from './runtime/server/middleware.js';
3
3
  import { ZitadelProject } from '@zitadel/api/config';
4
- export { ClientAuthResult } from './runtime/types.js';
5
- export { AuthResult, NextgenMiddlewareOptions } from '@zitadel/sdk-core/middleware';
4
+ export { AuthResult, ClientAuthResult, NextgenMiddlewareOptions } from '@zitadel/sdk-core/middleware';
6
5
  import 'h3';
7
6
 
8
7
  /**
package/dist/index.js CHANGED
@@ -2,10 +2,11 @@ import "./chunk-ZNXAFGLP.js";
2
2
  import {
3
3
  createNextgenMiddleware,
4
4
  getAuth
5
- } from "./chunk-AQZKZRKQ.js";
5
+ } from "./chunk-MJ3RLEU6.js";
6
6
 
7
7
  // src/index.ts
8
8
  import { ZitadelLogin, ZitadelLogout } from "@zitadel/components";
9
+ import { businessLocales } from "@zitadel/components";
9
10
 
10
11
  // src/runtime/composables/useZitadelProject.ts
11
12
  import { getZitadelConfig } from "@zitadel/api/config";
@@ -15,6 +16,7 @@ function useZitadelProject() {
15
16
  export {
16
17
  ZitadelLogin,
17
18
  ZitadelLogout,
19
+ businessLocales,
18
20
  createNextgenMiddleware,
19
21
  getAuth,
20
22
  useZitadelProject
package/dist/module.js CHANGED
@@ -48,7 +48,7 @@ var module_default = defineNuxtModule({
48
48
  // Server-only — never exposed to the client. Override at deploy time via
49
49
  // `NUXT_NEXTGEN_PROJECT_SECRET`. Left undefined when the env var (and
50
50
  // `.env.local`) provide no value, so the middleware surfaces a missing
51
- // bearer instead of silently sending an empty one.
51
+ // exchange bearer instead of silently sending an empty one.
52
52
  projectSecret: loadProjectSecret(nuxt.options.rootDir)
53
53
  };
54
54
  nuxt.options.runtimeConfig.public.zitadelProxyPath = options.proxyPath ?? "/__nextgen";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createNextgenMiddleware
3
- } from "../../chunk-AQZKZRKQ.js";
3
+ } from "../../chunk-MJ3RLEU6.js";
4
4
 
5
5
  // src/runtime/server/handler.ts
6
6
  import { useRuntimeConfig } from "#imports";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createNextgenMiddleware,
3
3
  getAuth
4
- } from "../../chunk-AQZKZRKQ.js";
4
+ } from "../../chunk-MJ3RLEU6.js";
5
5
  export {
6
6
  createNextgenMiddleware,
7
7
  getAuth
@@ -1,37 +1 @@
1
- import { UnauthState } from '@zitadel/sdk-core/middleware';
2
- export { AuthResult, AuthState, NextgenMiddlewareOptions, NextgenSession, UnauthState } from '@zitadel/sdk-core/middleware';
3
-
4
- /**
5
- * Re-exports shared SDK types from `@zitadel/sdk-core`.
6
- *
7
- * These types are defined once in sdk-core and shared by both sdk-next
8
- * and sdk-nuxt.
9
- */
10
-
11
- /**
12
- * The client-safe session exposed to Vue components via {@link useAuth}.
13
- * Identical to {@link NextgenSession} but omits `token` — the raw JWT must
14
- * not be serialised into the Nuxt SSR payload where third-party scripts can
15
- * read it.
16
- */
17
- type ClientSession = {
18
- /** The user's unique identifier (`sub` claim). */
19
- userId: string;
20
- /** The user's email address, or `null` if not present in the token. */
21
- email: string | null;
22
- /** The user's display name, or `null` if not present in the token. */
23
- name: string | null;
24
- };
25
- /** Client-safe auth state when the user is signed in. */
26
- type ClientAuthState = {
27
- isAuthenticated: true;
28
- session: ClientSession;
29
- };
30
- /**
31
- * Union of all possible auth states returned by {@link useAuth}.
32
- * Token is intentionally absent — use {@link getAuth} server-side when the
33
- * raw JWT is needed.
34
- */
35
- type ClientAuthResult = ClientAuthState | UnauthState;
36
-
37
- export type { ClientAuthResult, ClientAuthState, ClientSession };
1
+ export { AuthResult, AuthState, ClientAuthResult, ClientAuthState, ClientSession, NextgenMiddlewareOptions, NextgenSession, UnauthState } from '@zitadel/sdk-core/middleware';
package/dist/server.js CHANGED
@@ -2,7 +2,7 @@ import "./chunk-ZNXAFGLP.js";
2
2
  import {
3
3
  createNextgenMiddleware,
4
4
  getAuth
5
- } from "./chunk-AQZKZRKQ.js";
5
+ } from "./chunk-MJ3RLEU6.js";
6
6
  export {
7
7
  createNextgenMiddleware,
8
8
  getAuth
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zitadel/sdk-nuxt",
3
- "version": "0.1.0-alpha.17",
3
+ "version": "0.1.0-alpha.19",
4
4
  "description": "Nuxt proxy middleware and helpers for the pre-release Zitadel preview.",
5
5
  "homepage": "https://github.com/zitadel/nextgen/tree/main/packages/sdk-nuxt#readme",
6
6
  "bugs": {
@@ -33,9 +33,9 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@zitadel/api": "0.1.0-alpha.17",
37
- "@zitadel/sdk-core": "0.1.0-alpha.17",
38
- "@zitadel/components": "0.1.0-alpha.17"
36
+ "@zitadel/api": "0.1.0-alpha.19",
37
+ "@zitadel/sdk-core": "0.1.0-alpha.19",
38
+ "@zitadel/components": "0.1.0-alpha.19"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "h3": ">=1",
@@ -53,7 +53,7 @@
53
53
  "eslint-plugin-perfectionist": "^4.0.0",
54
54
  "eslint-plugin-prettier": "^5.0.0",
55
55
  "h3": "^1.15.3",
56
- "nuxt": "^4.4.8",
56
+ "nuxt": "^4.5.1",
57
57
  "prettier": "^3.0.0",
58
58
  "tsup": "^8.3.5",
59
59
  "typescript": "^5.7.3",