@zitadel/cli 0.3.0 → 0.4.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.
- package/README.md +39 -0
- package/dist/index.mjs +1450 -1047
- package/dist/index.mjs.map +1 -1
- package/dist/lib/oclif/help.mjs +3 -2
- package/dist/lib/oclif/help.mjs.map +1 -1
- package/dist/skills/zitadel/SKILL.md +111 -0
- package/dist/skills/zitadel-cli/SKILL.md +635 -0
- package/dist/skills/zitadel-cli/references/driving-login-ui.md +124 -0
- package/dist/skills/zitadel-components/SKILL.md +152 -0
- package/dist/skills/zitadel-sdk-angular/SKILL.md +149 -0
- package/dist/skills/zitadel-sdk-next/SKILL.md +174 -0
- package/dist/skills/zitadel-sdk-nuxt/SKILL.md +149 -0
- package/dist/skills/zitadel-sdk-qwik/SKILL.md +136 -0
- package/dist/skills/zitadel-sdk-qwik-router/SKILL.md +126 -0
- package/dist/skills/zitadel-sdk-react/SKILL.md +149 -0
- package/dist/skills/zitadel-sdk-solid/SKILL.md +144 -0
- package/dist/skills/zitadel-sdk-solid-start/SKILL.md +149 -0
- package/dist/skills/zitadel-sdk-svelte/SKILL.md +137 -0
- package/dist/skills/zitadel-sdk-sveltekit/SKILL.md +147 -0
- package/dist/skills/zitadel-sdk-tanstack-start/SKILL.md +150 -0
- package/dist/skills/zitadel-sdk-vue/SKILL.md +147 -0
- package/dist/{skills-hint-N1H5-ny1.mjs → skills-hint-C8oe8cCo.mjs} +1446 -231
- package/dist/skills-hint-C8oe8cCo.mjs.map +1 -0
- package/oclif.manifest.json +102 -3
- package/package.json +8 -5
- package/skills/zitadel-cli/SKILL.md +62 -9
- package/dist/skills-hint-N1H5-ny1.mjs.map +0 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
### Driving the login UI
|
|
2
|
+
|
|
3
|
+
`<zitadel-login>`, `<zitadel-session>`, and `<zitadel-logout>` are Lit elements
|
|
4
|
+
with open shadow roots. The stable automation hooks live inside nested shadow roots, so a flat
|
|
5
|
+
`document.querySelector('[data-testid="zitadel-input-email"]')` will not find
|
|
6
|
+
the native control. Browser drivers with shadow-DOM-aware locators, such as
|
|
7
|
+
Playwright, can target the hooks directly. Generic DOM-eval drivers should
|
|
8
|
+
pierce shadow roots recursively:
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
function deepQuery(sel, root = document) {
|
|
12
|
+
const hit = root.querySelector(sel);
|
|
13
|
+
if (hit) return hit;
|
|
14
|
+
for (const el of root.querySelectorAll("*")) {
|
|
15
|
+
if (el.shadowRoot) {
|
|
16
|
+
const result = deepQuery(sel, el.shadowRoot);
|
|
17
|
+
if (result) return result;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Use host hooks such as `zitadel-field-email`, `zitadel-field-password`, and
|
|
25
|
+
`zitadel-action-submit` when targeting the Lit atoms. Use native shadow-control
|
|
26
|
+
hooks such as `zitadel-input-email`, `zitadel-input-password`, and
|
|
27
|
+
`zitadel-action-submit-button` when filling or clicking the underlying input or
|
|
28
|
+
button. Hooks stay method-named even when the flow engine names a credential
|
|
29
|
+
field `x-auth-methods#<method>`; only the `name` attribute carries that raw
|
|
30
|
+
form key. Enter inside a field submits the step's primary action, but only for
|
|
31
|
+
key events that carry `key: "Enter"` — drivers whose synthesized key events
|
|
32
|
+
omit it (some CDP wrappers) should click `zitadel-action-submit` instead.
|
|
33
|
+
|
|
34
|
+
For sign-out, the profile page that setup generates renders `<zitadel-session>`:
|
|
35
|
+
click its `zitadel-session-logout` host, or the native button inside it,
|
|
36
|
+
`zitadel-session-logout-button`. An app that renders `<zitadel-logout>` instead
|
|
37
|
+
hides sign-out behind an avatar menu: click the "Open user menu" button, then
|
|
38
|
+
pierce to `.signout-btn` (Playwright: `zitadel-logout .signout-btn`), which
|
|
39
|
+
carries no hook. A `<zitadel-logout>` with a `<template>` child shows no menu:
|
|
40
|
+
click the app's own element marked `data-action="logout"`. The canonical
|
|
41
|
+
component hook list lives in `packages/components/README.md`.
|
|
42
|
+
|
|
43
|
+
The checked-in automated regression path is `moon run e2e:journey`, which
|
|
44
|
+
exercises fresh-app setup plus registration, logout, and login across the
|
|
45
|
+
supported frameworks.
|
|
46
|
+
|
|
47
|
+
Repo config is authoritative: edit `zitadel.json` or files under `.zitadel/`,
|
|
48
|
+
then re-run `deploy` (or `deploy --dry-run` to validate and preview the diff
|
|
49
|
+
without publishing). Schema and flow files are synced from
|
|
50
|
+
`.zitadel/schemas/*.json` and `.zitadel/flows/*.json`. Login templates
|
|
51
|
+
(branding) are synced from `.zitadel/branding/`: a single `branding.json`
|
|
52
|
+
descriptor (layout, asset URLs) plus a sibling `login.liquid` LiquidJS
|
|
53
|
+
template referenced as `"liquid_template": { "$file": "./login.liquid" }`. Scaffold them with the
|
|
54
|
+
`branding eject` command (`--design centered|minimal`,
|
|
55
|
+
interactive picker on a TTY); setup never scaffolds them. Branding is
|
|
56
|
+
revisioned and immutable: every edit — including a `.liquid`-only edit —
|
|
57
|
+
is published as part of the next release when you `deploy`, and the login
|
|
58
|
+
serves the newest release. `deploy` validates templates with the authoritative
|
|
59
|
+
LiquidJS validator (and `deploy --dry-run` runs the same validation without
|
|
60
|
+
publishing; `E_VALIDATION` lists rule ids such as `no-script-tag` and
|
|
61
|
+
`mandatory-gates`; every template must keep a `{% mandatory_gates %}` tag,
|
|
62
|
+
where the login adds any required field and the primary action the template
|
|
63
|
+
left out). Asset URLs must be absolute `https://`; during development, images
|
|
64
|
+
may use `http://` on `localhost`, `127.x.x.x`, or `[::1]` (not `*.localhost`).
|
|
65
|
+
`typography.font_url` needs a `typography.font_family`, is always `https://`,
|
|
66
|
+
and is loaded only when the login runs as `variant="page"`, which setup
|
|
67
|
+
generates only when it creates a Next.js or Nuxt app itself, in an empty
|
|
68
|
+
directory. Keep exactly one descriptor in
|
|
69
|
+
`.zitadel/branding/` — extra `*.json` files there fail the scan.
|
|
70
|
+
Server-provisioned defaults remain a fallback for non-CLI project
|
|
71
|
+
creation, but CLI-created projects are authored from local files first. Flows are
|
|
72
|
+
revisioned like branding: an edit is published as a new immutable flow revision
|
|
73
|
+
in the next `deploy`. A flow names its schema by `objectType` in `user_schema`
|
|
74
|
+
and runs against the schema's newest revision, so a schema revise needs no
|
|
75
|
+
change to `user_schema`; one that removes or renames a property a step
|
|
76
|
+
collects needs that step's `fields` updated, and `deploy` checks each flow
|
|
77
|
+
against the schema it names. A login pinned by `flow-name` serves that flow's
|
|
78
|
+
newest active revision; an unpinned login serves the newest active unscoped
|
|
79
|
+
revision for its purpose in the project, whatever its flow's name. Older
|
|
80
|
+
revisions stay active, so an `audience` added after a flow was deployed
|
|
81
|
+
without one doesn't take that earlier revision out of the running. Removing a
|
|
82
|
+
flow file does not retire the
|
|
83
|
+
flow. Operation policies live in `.zitadel/policies/`: one `<operation>.json`
|
|
84
|
+
per guarded operation, named after its `operation`, holding
|
|
85
|
+
`{"$schema": "../meta/policy.json", "operation": "auth.factor.password.set",
|
|
86
|
+
"config": {"min_length": 15}}`. `deploy` checks each file against
|
|
87
|
+
`.zitadel/meta/policy.json` (known operation, only the template's settings,
|
|
88
|
+
within their bounds) and its name before anything is sent; settings that
|
|
89
|
+
contradict each other (a `min_length` above `max_length`) are refused by the
|
|
90
|
+
server's release build with `rel.invalid`, the policy's entry carrying
|
|
91
|
+
`pol.invalid`. A changed file publishes a new revision of the same policy,
|
|
92
|
+
which applies the next time a password is set. A setting left out is off, and
|
|
93
|
+
`"config": {}` switches every rule off. A sign-in or sign-up uses the policy
|
|
94
|
+
the deployed release carries, so deploying without the file switches it off
|
|
95
|
+
there; a password set through the API, or before the first deploy, uses the
|
|
96
|
+
newest revision, which only `"config": {}` switches off. `policies list` and
|
|
97
|
+
`policies get <id|operation>` read them back. A policy's cases live beside it
|
|
98
|
+
in `<operation>.test.json` (`.zitadel/meta/policy-test.json`): each names an
|
|
99
|
+
`input` in the operation's terms (`{"password": "…"}`), optional `facts` the
|
|
100
|
+
operation would look up (`self_service`, `history_matches`), and the expected
|
|
101
|
+
decision (`allowed`, optionally `code` and exactly the `violations`). The
|
|
102
|
+
decision and rules hold in the login too; `code` is the API's answer, and a
|
|
103
|
+
flow shows each rule as `error.x-auth-methods#password_<rule>` instead.
|
|
104
|
+
`policies test [operation]` evaluates every case on the server against the
|
|
105
|
+
sibling policy file, or with `--release <id>` against the revision that
|
|
106
|
+
release pins; a missing policy is tested as none. It fails with `E_VALIDATION`
|
|
107
|
+
(exit 3) when a case does not hold, `data`/`details` carrying `source`,
|
|
108
|
+
`operations[].cases[]` (`name`, `passed`, `expected`, `actual`), `passed` and
|
|
109
|
+
`failed`; with no test files it is `skipped` (`no-policy-tests`). `deploy`
|
|
110
|
+
never carries test files. Until #2331, a release that pins no flow is tested
|
|
111
|
+
with a warning: its sign-ins use the newest revisions instead. Write cases from
|
|
112
|
+
what the user wants enforced; never invent expectations for them. `setup` writes and publishes
|
|
113
|
+
the password policy with a `min_length` of 15; pass `--password-min-length <n>`
|
|
114
|
+
only when the user asks for another value (`0` sets none). Managed files carry
|
|
115
|
+
a marker comment; `eject` removes only
|
|
116
|
+
files that still carry it, preserving anything the user replaced. For app-local
|
|
117
|
+
development, `--server local` resolves through `.zitadel/local/runtime.json` and
|
|
118
|
+
requires a healthy
|
|
119
|
+
`npx @zitadel/cli@latest start` runtime. The `.zitadel/local/` that `start`
|
|
120
|
+
writes into the directory still counts as empty, so a same-directory `setup`
|
|
121
|
+
scaffolds without `--force` (this is exactly what the golden path does).
|
|
122
|
+
`setup` installs dependencies
|
|
123
|
+
with the detected package manager by default; pass `--skip-install` when the
|
|
124
|
+
agent or host workflow will install dependencies separately.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zitadel-components
|
|
3
|
+
description: >-
|
|
4
|
+
Embed, theme, customise, or drive the Zitadel login web component directly —
|
|
5
|
+
the framework-agnostic `<zitadel-login>` / `<zitadel-logout>` Lit elements
|
|
6
|
+
from `@zitadel/components`. Use when dropping the login widget onto a plain
|
|
7
|
+
HTML page, styling it with `--zl-*` design tokens or `::part(...)` hooks,
|
|
8
|
+
pinning its theme/variant/locale, wiring its flow events, or automating it in
|
|
9
|
+
a browser via its shadow-DOM hooks — i.e. using the Zitadel login widget
|
|
10
|
+
itself, outside any framework SDK wrapper.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# @zitadel/components
|
|
14
|
+
|
|
15
|
+
Lit web components for the Zitadel auth UI: the `<zitadel-login>` orchestrator
|
|
16
|
+
(a drop-in element that calls the flow API and renders every step),
|
|
17
|
+
`<zitadel-logout>` / `<zitadel-session>`, and the `<zl-*>` atoms.
|
|
18
|
+
|
|
19
|
+
## When to use
|
|
20
|
+
|
|
21
|
+
Use this skill when you embed, theme, or automate the login/logout web
|
|
22
|
+
component **directly** — on a static page, in a server-rendered app, or inside
|
|
23
|
+
any shell where you hold the custom element yourself. The framework SDK skills
|
|
24
|
+
(Next, Nuxt, React, etc.) wrap this same element for a specific framework; reach
|
|
25
|
+
for those when the user is in that framework. Reach for this one when the work
|
|
26
|
+
is the component itself: attributes, tokens, parts, events, shadow-DOM hooks.
|
|
27
|
+
|
|
28
|
+
## Install / use
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
corepack pnpm add @zitadel/components@latest
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`lit`, `liquidjs`, `dompurify` are externalised peer/runtime deps (consumers
|
|
35
|
+
dedupe their own copies). A CDN `standalone` build is published (`unpkg` /
|
|
36
|
+
`jsdelivr` → `./dist/standalone.mjs`). Subpath exports exist for `./atoms`,
|
|
37
|
+
`./orchestrator`, `./tokens`, `./manifests`, and JSX types at `./jsx`. Importing
|
|
38
|
+
the barrel side-effect-registers every element; import leaf subpaths to
|
|
39
|
+
tree-shake.
|
|
40
|
+
|
|
41
|
+
## Embed & configure
|
|
42
|
+
|
|
43
|
+
Configure the SDK once, then drop the element — it reads the global project
|
|
44
|
+
handle from `configureZitadel()` via `getZitadelConfig()`:
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<script type="module">
|
|
48
|
+
import '@zitadel/components';
|
|
49
|
+
import { configureZitadel } from '@zitadel/api/config';
|
|
50
|
+
configureZitadel({ projectId: 'proj_123', proxyPath: '/__nextgen' });
|
|
51
|
+
</script>
|
|
52
|
+
<zitadel-login id="login" variant="page" purpose="login"></zitadel-login>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`<zitadel-login>` props (the authoritative list is in the README table and the
|
|
56
|
+
TS types — discover, don't assume): `variant` (`widget` default: content-sized,
|
|
57
|
+
transparent, no font injection, no focus grab / `page`: full-page chrome),
|
|
58
|
+
`theme` (`light`/`dark`/`auto`, resolved value lands on `data-theme`), `purpose`
|
|
59
|
+
(`login`/`register`/`reset_password`/…), `flow-name`, `project` (object property,
|
|
60
|
+
not an attribute — assign via `ref`/JS, not markup), `lang` + `locales`,
|
|
61
|
+
`post-sign-in-url` (`force-post-sign-in-url`: see below), `resume-flow-id`,
|
|
62
|
+
`preview-state` / `preview-success-step`.
|
|
63
|
+
After sign-in the widget returns to the path in the page's `next` query
|
|
64
|
+
parameter, which every server SDK's middleware sets when it sends a signed-out
|
|
65
|
+
visit to a protected route to the login page, and goes to `post-sign-in-url`
|
|
66
|
+
when there is none or it is not a path on the page's origin. A page that uses
|
|
67
|
+
`next` for something of its own sets `force-post-sign-in-url`.
|
|
68
|
+
It emits `zitadel-flow-input`, `-step`, `-complete`, `-error`, `-redirect`
|
|
69
|
+
events (`-step` fires for every step including the first; `-redirect` is a
|
|
70
|
+
non-cancelable notification — the widget navigates to the IdP itself).
|
|
71
|
+
|
|
72
|
+
`<zitadel-logout>`: `project`, `post-sign-out-url`; supports a light-DOM
|
|
73
|
+
`<template>` slot for a custom menu.
|
|
74
|
+
|
|
75
|
+
Theming levers (strongest-first resolution, documented in the README):
|
|
76
|
+
|
|
77
|
+
- **Host CSS tokens** — set `--zl-*` custom properties in your own stylesheet on
|
|
78
|
+
`zitadel-login { ... }`. These inherit across shadow boundaries and **outrank**
|
|
79
|
+
both the design-system defaults and server-side tenant branding. The stable
|
|
80
|
+
names are listed in
|
|
81
|
+
[Customize the login design](https://preview.zitadel.com/docs/guides/customize-design.md).
|
|
82
|
+
- **`::part(...)`** — e.g. `zitadel-login::part(form)`, `::part(field-input)`,
|
|
83
|
+
`::part(button-root)`. Atom parts forward through the orchestrator as
|
|
84
|
+
`<atom>-<part>`; bare names (`zl-field::part(input)`) apply when composing
|
|
85
|
+
atoms directly.
|
|
86
|
+
- **`variant` + `--zl-page-min-height`** for sizing/placement; width-responsive
|
|
87
|
+
chrome keys off the widget's own width (container queries).
|
|
88
|
+
- Server branding payload, and a tenant Liquid template via the payload's
|
|
89
|
+
`liquid_template` field (a declarative `template` attribute is not yet
|
|
90
|
+
exposed).
|
|
91
|
+
- **`suppress-header`** (boolean attribute, reflected) hides the widget's own
|
|
92
|
+
heading — use it when the host page already shows a title and you want to
|
|
93
|
+
avoid a duplicate. The login moves between sign-in and sign-up by itself, so
|
|
94
|
+
that title must follow its `zitadel-flow-step` event: `step.purpose` is
|
|
95
|
+
`login` or `register`.
|
|
96
|
+
Otherwise control chrome through `variant` and tokens.
|
|
97
|
+
- **`suppress-password-toggle`** (boolean attribute) hides the show/hide button
|
|
98
|
+
the login shows on password fields by default. It applies to custom Liquid
|
|
99
|
+
templates too; templates have nothing to wire.
|
|
100
|
+
|
|
101
|
+
Automation hooks (stable `data-testid`s the default template emits): host atoms
|
|
102
|
+
`zitadel-field-email` / `zitadel-field-password` / `zitadel-action-submit`;
|
|
103
|
+
native shadow controls `zitadel-input-email` / `zitadel-input-password` /
|
|
104
|
+
`zitadel-action-submit-button`. Hooks are method-named even when the flow engine
|
|
105
|
+
names a credential field `x-auth-methods#<method>` — the `name` attribute keeps
|
|
106
|
+
the raw wire key; only the hook is normalised (`hookName` in
|
|
107
|
+
`src/internal/hook-name.ts`). For sign-out, the button inside `<zitadel-session>`
|
|
108
|
+
(the signed-in card setup's profile pages render) carries
|
|
109
|
+
`zitadel-session-logout`, and its native button `zitadel-session-logout-button`.
|
|
110
|
+
`<zitadel-logout>` has no hook: open its avatar menu ("Open user menu"), then
|
|
111
|
+
pierce to `.signout-btn`. Given a `<template>` child, it shows no menu, and the
|
|
112
|
+
element marked `data-action="logout"` signs out.
|
|
113
|
+
|
|
114
|
+
## Read these, don't guess
|
|
115
|
+
|
|
116
|
+
The component surface moves faster than any list here, so treat these as
|
|
117
|
+
authoritative and discover the current shape first:
|
|
118
|
+
|
|
119
|
+
- `node_modules/@zitadel/components/README.md` — the element API tables, the
|
|
120
|
+
customisation tiers, and the **canonical hook list** live here.
|
|
121
|
+
- The published TypeScript types (`./dist/index.d.mts` and the `./atoms`,
|
|
122
|
+
`./orchestrator`, `./tokens`, `./jsx` subpaths) — exact prop/event/manifest
|
|
123
|
+
shapes.
|
|
124
|
+
- [Customize the login design](https://preview.zitadel.com/docs/guides/customize-design.md)
|
|
125
|
+
— the stable `--zl-*` tokens and the `::part()` names the login forwards.
|
|
126
|
+
- Per-atom manifests (`@zitadel/components/manifests`) — the allowed
|
|
127
|
+
attributes/parts/events for each atom.
|
|
128
|
+
|
|
129
|
+
Read the pattern, then confirm the specific attribute/part/token/event against
|
|
130
|
+
these before writing it — don't reproduce a catalogue from memory.
|
|
131
|
+
|
|
132
|
+
## Gotchas
|
|
133
|
+
|
|
134
|
+
- **Server rendering.** If your app renders on the server, mount the widget on the client only, after hydration, or give a raw `<zitadel-login>` element `project-id` and `proxy-path` as attributes, because a widget that starts before its configuration arrives shows an error instead of the flow.
|
|
135
|
+
- **Open shadow roots, nested.** The automation hooks sit inside nested shadow
|
|
136
|
+
roots, so a flat `document.querySelector('[data-testid="zitadel-input-email"]')`
|
|
137
|
+
misses them. Use a shadow-aware locator (Playwright) or recurse through
|
|
138
|
+
`el.shadowRoot`. Enter submits the step only for key events carrying
|
|
139
|
+
`key: "Enter"`; CDP wrappers that omit it should click `zitadel-action-submit`.
|
|
140
|
+
- **Same-origin proxy.** The element calls the typed `@zitadel/api` fetch client
|
|
141
|
+
directly with `credentials: "include"`; the stateless server's `_zflow`
|
|
142
|
+
HttpOnly cookie is the source of truth between requests. There is no transport
|
|
143
|
+
to swap — mock at the network layer (`@zitadel/api-mock` MSW handlers) for
|
|
144
|
+
offline/test.
|
|
145
|
+
- **Theming through shadow DOM.** `--zl-*` custom properties inherit across
|
|
146
|
+
shadow boundaries, so host-page token overrides reach the atoms' own shadow
|
|
147
|
+
roots and win over `:host` defaults and server branding — deliberate, so an
|
|
148
|
+
embedding app matches its design system without a server round-trip. Internals
|
|
149
|
+
you can't reach with a token need `::part(...)`, not a descendant selector.
|
|
150
|
+
- **`project` is a property, not an attribute** — it's a typed object, so set it
|
|
151
|
+
via JS/`ref` (or configure globally and let the element read it), never as
|
|
152
|
+
markup.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zitadel-sdk-angular
|
|
3
|
+
description: >-
|
|
4
|
+
Integrate Zitadel authentication into an Angular app with
|
|
5
|
+
@zitadel/sdk-angular — the client-side SPA components for login,
|
|
6
|
+
registration, logout, and the signed-in session card. Use when adding or
|
|
7
|
+
wiring Zitadel auth, a login or registration screen, or session handling into
|
|
8
|
+
an Angular app, and especially when the app already exists and the CLI's
|
|
9
|
+
setup stops on it (then wire it in by hand with this skill).
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# @zitadel/sdk-angular
|
|
13
|
+
|
|
14
|
+
Angular wrapper components for the Zitadel auth UI in a client-side SPA.
|
|
15
|
+
|
|
16
|
+
## When to use
|
|
17
|
+
|
|
18
|
+
You are adding Zitadel login, registration, logout, or session handling to an
|
|
19
|
+
Angular app and wiring the auth UI in by hand.
|
|
20
|
+
|
|
21
|
+
The `zitadel` CLI scaffolds a *fresh* Angular app
|
|
22
|
+
(`npx @zitadel/cli@latest setup --framework angular`) with this SDK already wired in. In an
|
|
23
|
+
**existing** Angular app it writes the root component to `src/app/app.ts`, and
|
|
24
|
+
stops with `E_CONFLICT` when the app has one there, or when `src/main.ts`
|
|
25
|
+
bootstraps a root from another file (Angular 17–19's
|
|
26
|
+
`src/app/app.component.ts`). When setup stops, follow this skill to install the
|
|
27
|
+
package and wire the components in manually.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm install @zitadel/sdk-angular@latest
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Peer dependencies: `@angular/core` and `@angular/common`, both `>=17`. Requires TypeScript ≥ 5.0 (the published types use `export type *`).
|
|
36
|
+
|
|
37
|
+
## Integrate
|
|
38
|
+
|
|
39
|
+
The components are **standalone** — import them directly into a component's
|
|
40
|
+
`imports`. There is no NgModule, provider, or route guard to register.
|
|
41
|
+
|
|
42
|
+
1. Build a project handle once with `configureZitadel(...)` and pass it to the
|
|
43
|
+
components via the `[project]` input:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Component } from '@angular/core';
|
|
47
|
+
import { ZitadelLoginComponent, configureZitadel } from '@zitadel/sdk-angular';
|
|
48
|
+
|
|
49
|
+
const project = configureZitadel({ projectId: '…', proxyPath: '/__nextgen' });
|
|
50
|
+
|
|
51
|
+
@Component({
|
|
52
|
+
standalone: true,
|
|
53
|
+
imports: [ZitadelLoginComponent],
|
|
54
|
+
template: `<zitadel-auth-login
|
|
55
|
+
[project]="project"
|
|
56
|
+
purpose="login"
|
|
57
|
+
postSignInUrl="/"
|
|
58
|
+
/>`,
|
|
59
|
+
})
|
|
60
|
+
export class LoginPage {
|
|
61
|
+
project = project;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
2. Logout and the signed-in session card follow the same shape:
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<zitadel-auth-logout [project]="project" postSignOutUrl="/login" />
|
|
69
|
+
<zitadel-auth-session [project]="project" postSignOutUrl="/login" />
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
3. Wire them into your routes as ordinary standalone components, e.g. a
|
|
73
|
+
`/login` route rendering `LoginPage` and a `/logout` route rendering a
|
|
74
|
+
component that imports `ZitadelLogoutComponent`. `purpose="login"` vs
|
|
75
|
+
`purpose="register"` on `<zitadel-auth-login>` selects sign-in vs
|
|
76
|
+
registration; `postSignInUrl` / `postSignOutUrl` are where the widget sends
|
|
77
|
+
the user afterwards.
|
|
78
|
+
|
|
79
|
+
4. The widgets call `${proxyPath}/…` **same-origin** (default `/__nextgen`). For
|
|
80
|
+
local development you can instead point `proxyPath` straight at the backend
|
|
81
|
+
(cross-origin), e.g. `proxyPath: 'http://localhost:8080'` (the `zitadel start`
|
|
82
|
+
local runtime default port).
|
|
83
|
+
|
|
84
|
+
## Widget props and events
|
|
85
|
+
|
|
86
|
+
`ZitadelLogin`, `ZitadelLogout` and `ZitadelSession` take the props below, spelled the same in every SPA SDK. An unset prop keeps the widget's own default.
|
|
87
|
+
|
|
88
|
+
| Prop | Type | Default | Widgets | Description |
|
|
89
|
+
| --- | --- | --- | --- | --- |
|
|
90
|
+
| `project` | `ZitadelProject` | unset | Login, Logout, Session | The handle from `configureZitadel(...)`. Wins over `projectId` and `proxyPath`. |
|
|
91
|
+
| `projectId` | `string` | unset | Login, Logout, Session | Project id, read when no `project` handle is passed. |
|
|
92
|
+
| `proxyPath` | `string` | `"/__nextgen"` | Login, Logout, Session | Same-origin path the widget calls. |
|
|
93
|
+
| `purpose` | `CreateFlowBodyPurpose` | `"login"` | Login | Flow purpose. |
|
|
94
|
+
| `flowName` | `string` | unset | Login | Runs the flow definition with this `name`; unset runs the project's default flow for the purpose. |
|
|
95
|
+
| `postSignInUrl` | `string` | unset | Login | Where to navigate after a completed sign-in. When the page's `next` query parameter names a path on the same origin, as the server SDKs' middleware sets it for a signed-out visit to a protected route, the widget returns there instead. |
|
|
96
|
+
| `forcePostSignInUrl` | `boolean` | `false` | Login | Navigates to `postSignInUrl` even when the page's `next` query parameter names a path, for a page that uses `next` for something of its own. |
|
|
97
|
+
| `postSignOutUrl` | `string` | unset | Logout, Session | Where to navigate after sign-out. |
|
|
98
|
+
| `resumeFlowId` | `string` or `null` | unset | Login | An existing flow's id to resume instead of starting a new flow; unset, `null` or `""` falls back to the `flow` query parameter, and an id that no longer resolves starts a new flow. |
|
|
99
|
+
| `variant` | `"widget"` or `"page"` | `"widget"` | Login, Session | `widget` is content-sized and paints no page chrome; `page` claims the viewport, paints the surface background and loads the brand font, and on Login focuses the first field. |
|
|
100
|
+
| `theme` | `"light"`, `"dark"` or `"auto"` | unset | Login, Logout, Session | Colour mode. Unset follows the tenant's branding theme on Login, then the variant default (`dark` for `page`, `auto` for `widget`); Logout defaults to `auto`. `auto` follows `prefers-color-scheme`. |
|
|
101
|
+
| `suppressHeader` | `boolean` | `false` | Login, Session | Visually hides the widget's own heading block while keeping it in the accessibility tree, for pages that already carry the heading. |
|
|
102
|
+
| `suppressPasswordToggle` | `boolean` | `false` | Login | Hides the show/hide button on password fields. |
|
|
103
|
+
| `heading` | `string` | `"Signed in as"` | Session | Heading text override. |
|
|
104
|
+
| `logoutLabel` | `string` | `"Sign out"` | Session | Logout action label override. |
|
|
105
|
+
| `locales` | `Record<string, Partial<Record<string, string>>>` | unset | Login | Copy overrides merged over the builtin dictionaries, keyed by primary language subtag, for example `{ en: { "identifier.title": "Welcome back" } }`. |
|
|
106
|
+
| `lang` | `string` | browser language | Login | BCP-47 language override. |
|
|
107
|
+
|
|
108
|
+
Each widget event reaches the callback in the last column, which receives the event's `detail` payload.
|
|
109
|
+
|
|
110
|
+
| Widget event | Fires when | Detail | Widgets | Callback |
|
|
111
|
+
| --- | --- | --- | --- | --- |
|
|
112
|
+
| `zitadel-flow-step` | Login shows a step, the first one included. | `{ step }` | Login | `(flowStep)` |
|
|
113
|
+
| `zitadel-flow-input` | The visitor edits a field. | `{ name, value }` | Login | `(flowInput)` |
|
|
114
|
+
| `zitadel-flow-complete` | The flow reaches a terminal step. | `{ behavior, redirect_uri?, handoff_token?, handoff_token_expires_at? }` | Login | `(flowComplete)` |
|
|
115
|
+
| `zitadel-flow-redirect` | The widget hands the browser to an identity provider; nobody is signed in yet. | `{ redirect_url, step }` | Login | `(flowRedirect)` |
|
|
116
|
+
| `zitadel-flow-error` | The flow reports an error. | `{ message }` | Login | `(flowError)` |
|
|
117
|
+
| `zitadel-signout` | The user signs out. | `{ display, identifier }` | Logout, Session | `(signout)` |
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<zitadel-auth-login [project]="project" theme="light" (flowComplete)="track($event)" />
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The `zitadel-flow-complete`, `zitadel-flow-redirect` and `zitadel-signout` details carry a `waitUntil(promise)`; call it from your handler to make the widget wait for your async work before it navigates.
|
|
124
|
+
|
|
125
|
+
## Read these, don't guess
|
|
126
|
+
|
|
127
|
+
This skill shows the *pattern*; it is not an API catalog and will drift. For
|
|
128
|
+
exact names, inputs, and props, treat the installed package as authoritative:
|
|
129
|
+
|
|
130
|
+
- `node_modules/@zitadel/sdk-angular/README.md` — the install, usage, and
|
|
131
|
+
proxy/deployment notes.
|
|
132
|
+
- The TypeScript types / exports (`node_modules/@zitadel/sdk-angular/dist/types/public-api.d.ts`)
|
|
133
|
+
— the exported symbols (`ZitadelLoginComponent`, `ZitadelLogoutComponent`,
|
|
134
|
+
`ZitadelSessionComponent`, `configureZitadel`, `ZitadelConfig`,
|
|
135
|
+
`ZitadelProject`, `businessLocales`) and every component `@Input()` /
|
|
136
|
+
`@Output()`. Read them before using an input you are unsure of.
|
|
137
|
+
|
|
138
|
+
## Gotchas
|
|
139
|
+
|
|
140
|
+
- **Server rendering.** If your app renders on the server, mount the widget on the client only, after hydration (the `zitadel-auth-login` wrapper sets `projectId` and `proxyPath` as element properties, so adding them to the wrapper does not put them in server markup), or use a raw `<zitadel-login>` element with `project-id` and `proxy-path` as attributes, because a widget that starts before its configuration arrives shows an error instead of the flow.
|
|
141
|
+
- **Selector is `zitadel-auth-login`, not `zitadel-login`.** The wrapper's
|
|
142
|
+
selector is `zitadel-auth-login` (and `zitadel-auth-logout`,
|
|
143
|
+
`zitadel-auth-session`); `<zitadel-login>` is the underlying custom element
|
|
144
|
+
the wrapper renders internally — don't use it directly.
|
|
145
|
+
- **Production SPA deployment is not yet supported.** The same-origin
|
|
146
|
+
`${proxyPath}` must be provided by your hosting platform, and the CLI cannot
|
|
147
|
+
yet scaffold those configs. The CLI dev proxy covers local development only.
|
|
148
|
+
- **Session card export name.** `<zitadel-auth-session>` is exported as
|
|
149
|
+
`ZitadelSessionComponent`.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zitadel-sdk-next
|
|
3
|
+
description: >-
|
|
4
|
+
Integrate Zitadel authentication into a Next.js App Router app with
|
|
5
|
+
@zitadel/sdk-next. Use whenever the user wants to add Zitadel login,
|
|
6
|
+
registration, logout, or session handling to a Next.js project — wiring the
|
|
7
|
+
auth middleware/proxy, reading sessions in Server or Client Components,
|
|
8
|
+
rendering the `<zitadel-login>` UI, or customizing an existing Zitadel + Next
|
|
9
|
+
integration.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# @zitadel/sdk-next
|
|
13
|
+
|
|
14
|
+
Next.js middleware and helpers that wire the Zitadel auth UI and session
|
|
15
|
+
verification into an App Router app.
|
|
16
|
+
|
|
17
|
+
## When to use
|
|
18
|
+
|
|
19
|
+
Use this when adding or customizing Zitadel auth in an **existing or custom**
|
|
20
|
+
Next.js app, or when you need to understand how the pieces fit together.
|
|
21
|
+
|
|
22
|
+
For a brand-new app, prefer the CLI: `npx @zitadel/cli@latest setup --framework next` scaffolds
|
|
23
|
+
the whole integration (middleware, login page, env) and wires it for you. Reach
|
|
24
|
+
for this skill when that scaffold already exists and you are editing it, or when
|
|
25
|
+
the app is bespoke and you are integrating by hand.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm add @zitadel/sdk-next@latest
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Peer deps the package expects: `next >=15.2.3`, `react >=18`, `react-dom >=18`.
|
|
34
|
+
TypeScript `>=5.0` (the SDK consumer floor; the package is built against a
|
|
35
|
+
newer TypeScript, but that does not raise the floor for your project).
|
|
36
|
+
|
|
37
|
+
## Integrate
|
|
38
|
+
|
|
39
|
+
The package has per-runtime entry points — import from the one that matches
|
|
40
|
+
where the code runs, not the root, from client modules:
|
|
41
|
+
|
|
42
|
+
| Import | Runs in | Provides |
|
|
43
|
+
| ------------------------------ | --------------------------------- | ---------------------------------- |
|
|
44
|
+
| `@zitadel/sdk-next/middleware` | Edge middleware | `nextgenMiddleware`, `createProxy` |
|
|
45
|
+
| `@zitadel/sdk-next/server` | Server Components, Route Handlers | `auth()`, `NextgenProvider` |
|
|
46
|
+
| `@zitadel/sdk-next/react` | Client Components | `useAuth()`, `AuthContextProvider` |
|
|
47
|
+
| `@zitadel/sdk-next/session` | Client Components | `getSession()` |
|
|
48
|
+
| `@zitadel/sdk-next/client` | Client boundary | `configureZitadel()`, web components |
|
|
49
|
+
| `@zitadel/sdk-next/components` | Client Components | `ZitadelLogin`, `ZitadelLogout`, `ZitadelSession` |
|
|
50
|
+
|
|
51
|
+
**1. Request-boundary proxy.** It proxies `/__nextgen/*` to the auth backend
|
|
52
|
+
(same-origin), verifies the session JWT via JWKS, and redirects unauthenticated
|
|
53
|
+
users on protected routes. **The filename and exported function depend on the
|
|
54
|
+
Next.js major version** — `proxy.ts`/`proxy` is Next 16+, while Next 15 uses the
|
|
55
|
+
request middleware:
|
|
56
|
+
|
|
57
|
+
- **Next 16+:** create `src/proxy.ts` exporting `proxy`.
|
|
58
|
+
- **Next 15:** create `src/middleware.ts` exporting `middleware`.
|
|
59
|
+
|
|
60
|
+
The body is the same either way (only the file and function name change):
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// Next 16+: src/proxy.ts, exporting `proxy`.
|
|
64
|
+
// Next 15: src/middleware.ts, exporting `middleware` (rename the function).
|
|
65
|
+
import { nextgenMiddleware } from '@zitadel/sdk-next/middleware';
|
|
66
|
+
import type { NextRequest } from 'next/server';
|
|
67
|
+
|
|
68
|
+
export function proxy(req: NextRequest) {
|
|
69
|
+
return nextgenMiddleware(req, {
|
|
70
|
+
url: process.env.ZITADEL_URL,
|
|
71
|
+
protectedRoutes: ['/admin', '/dashboard*'],
|
|
72
|
+
loginPath: '/login',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Every route but Next.js's build files: protectedRoutes alone decides what
|
|
77
|
+
// needs a sign-in. A protected route a narrower matcher leaves out stays public.
|
|
78
|
+
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] };
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
When unsure which the installed Next.js uses, check the CLI's own output — its
|
|
82
|
+
patcher emits `middleware.ts` for Next 15 and `proxy.ts` for Next 16+.
|
|
83
|
+
|
|
84
|
+
**2. Read the session server-side** with `auth()` (it re-verifies the tunnelled
|
|
85
|
+
token — the header alone is never trusted):
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { auth } from '@zitadel/sdk-next/server';
|
|
89
|
+
|
|
90
|
+
// Render <Greeting /> inside <Suspense>: with Cache Components on (the
|
|
91
|
+
// create-next-app default), Next.js refuses request data outside it.
|
|
92
|
+
async function Greeting() {
|
|
93
|
+
const session = await auth();
|
|
94
|
+
if (!session.isAuthenticated) return <p>Not signed in</p>;
|
|
95
|
+
return <p>Hello {session.session.display ?? session.session.userId}</p>;
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**3. Seed client components** by rendering `NextgenProvider` (server-only — it
|
|
100
|
+
strips the raw token before the server→client boundary) in your root layout,
|
|
101
|
+
then read with `useAuth()` in any client component. For chrome on pages the
|
|
102
|
+
matcher does not cover, read client-side with `getSession()` from
|
|
103
|
+
`@zitadel/sdk-next/session` instead — it fetches same-origin
|
|
104
|
+
`{proxyPath}/api/v1/sessions/me`.
|
|
105
|
+
|
|
106
|
+
**4. Login page.** Render `ZitadelLogin` from `@zitadel/sdk-next/components`
|
|
107
|
+
client-side only (via `next/dynamic` with `ssr: false`); call
|
|
108
|
+
`configureZitadel({ projectId, proxyPath })` from the same entry inside the
|
|
109
|
+
dynamic import, and set the `postSignInUrl` prop. `ZitadelLogout` and
|
|
110
|
+
`ZitadelSession` exist for logout and profile UI. Sign-in/out navigate to `post-sign-in-url` /
|
|
111
|
+
`post-sign-out-url`, so chrome re-reads on the next page load.
|
|
112
|
+
|
|
113
|
+
## Widget props and events
|
|
114
|
+
|
|
115
|
+
`ZitadelLogin`, `ZitadelLogout` and `ZitadelSession` from `@zitadel/sdk-next/components` are the `@zitadel/sdk-react` wrappers, so they take the same camelCase props, which the `@zitadel/sdk-react` skill lists. They set object props such as `project` and `locales` as element properties on React 18 and 19, through server-rendered hydration too. The table below lists the events and their callbacks.
|
|
116
|
+
|
|
117
|
+
Each widget event reaches the callback in the last column, which receives the event's `detail` payload.
|
|
118
|
+
|
|
119
|
+
| Widget event | Fires when | Detail | Widgets | Callback |
|
|
120
|
+
| --- | --- | --- | --- | --- |
|
|
121
|
+
| `zitadel-flow-step` | Login shows a step, the first one included. | `{ step }` | Login | `onFlowStep` |
|
|
122
|
+
| `zitadel-flow-input` | The visitor edits a field. | `{ name, value }` | Login | `onFlowInput` |
|
|
123
|
+
| `zitadel-flow-complete` | The flow reaches a terminal step. | `{ behavior, redirect_uri?, handoff_token?, handoff_token_expires_at? }` | Login | `onFlowComplete` |
|
|
124
|
+
| `zitadel-flow-redirect` | The widget hands the browser to an identity provider; nobody is signed in yet. | `{ redirect_url, step }` | Login | `onFlowRedirect` |
|
|
125
|
+
| `zitadel-flow-error` | The flow reports an error. | `{ message }` | Login | `onFlowError` |
|
|
126
|
+
| `zitadel-signout` | The user signs out. | `{ display, identifier }` | Logout, Session | `onSignout` |
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
<ZitadelLogin project={project} theme="light" onFlowComplete={(detail) => track(detail)} />
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Read these, don't guess
|
|
133
|
+
|
|
134
|
+
This skill is the **pattern**, not an API catalog — it deliberately omits exact
|
|
135
|
+
prop/option names so it does not drift with the package version. For the
|
|
136
|
+
authoritative, version-correct specifics:
|
|
137
|
+
|
|
138
|
+
- `node_modules/@zitadel/sdk-next/README.md` — the full setup walkthrough with
|
|
139
|
+
every code sample.
|
|
140
|
+
- The package's TypeScript types (`.d.ts` per entry point) — exact exported
|
|
141
|
+
names, component props, `configureZitadel()` config, and the full middleware
|
|
142
|
+
options table (`proxyPath`, `ignoredRoutes`, `audience`, `allowedAlgorithms`,
|
|
143
|
+
timeouts, …).
|
|
144
|
+
|
|
145
|
+
Confirm names against those before writing code.
|
|
146
|
+
|
|
147
|
+
## Gotchas
|
|
148
|
+
|
|
149
|
+
- **Never import the package root from a `"use client"` module.** The root
|
|
150
|
+
re-exports the server-only `auth()`; pulling it into a client graph fails the
|
|
151
|
+
build. Client code imports from `/react` or `/session` only.
|
|
152
|
+
- **`NextgenProvider` is server-only** and must not be re-exported through a
|
|
153
|
+
`"use client"` `providers.tsx` wrapper — that would leak the unstripped token
|
|
154
|
+
into the RSC flight payload. To seed from client state, use
|
|
155
|
+
`AuthContextProvider` from `/react` (it only takes the token-less shape).
|
|
156
|
+
- **Same-origin proxy is required.** The widgets and `getSession()` hit
|
|
157
|
+
`{proxyPath}/api/v1/sessions/me` on the same origin; the middleware `matcher` must
|
|
158
|
+
cover `{proxyPath}/:path*` or the proxy breaks.
|
|
159
|
+
- **A protected route must be in the matcher too.** Next.js runs the
|
|
160
|
+
middleware only on routes the matcher covers, so a `protectedRoutes` entry a
|
|
161
|
+
narrower matcher misses is never redirected. Keep the every-route matcher.
|
|
162
|
+
- **`auth()` only sees a session on routes the matcher covers.** A header on an
|
|
163
|
+
unmatched public page always looks signed out — use `getSession()` there.
|
|
164
|
+
- **A rejected `getSession()` means *unknown*, not signed out** (broken proxy /
|
|
165
|
+
network / 5xx). Render a neutral or error state, never the signed-out CTA.
|
|
166
|
+
- **Config handles:** `ZITADEL_URL` (backend, server-side),
|
|
167
|
+
`ZITADEL_PROJECT_ID` (server-side) and `NEXT_PUBLIC_ZITADEL_PROJECT_ID`
|
|
168
|
+
(passed to `configureZitadel`). If the middleware runs with custom
|
|
169
|
+
verification options, pass the same ones to `auth()` so both layers accept
|
|
170
|
+
the same tokens.
|
|
171
|
+
- **Sessions count only in the app's own project:** `projectId`, else
|
|
172
|
+
`ZITADEL_PROJECT_ID`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`. With none set,
|
|
173
|
+
nobody is signed in. A session of another project on the same host reads as
|
|
174
|
+
signed out.
|