@zitadel/config 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.
Files changed (56) hide show
  1. package/README.md +27 -0
  2. package/defaults/README-branding.md +96 -0
  3. package/defaults/README-flows.md +21 -4
  4. package/defaults/README-schemas.md +2 -2
  5. package/defaults/branding/centered/login.liquid +164 -0
  6. package/defaults/branding/hero/login.liquid +224 -0
  7. package/defaults/branding/minimal/login.liquid +154 -0
  8. package/defaults/branding/split/login.liquid +183 -0
  9. package/defaults/branding/split-right/login.liquid +183 -0
  10. package/defaults/default-human-user.json +2 -19
  11. package/defaults/default-login.json +21 -4
  12. package/defaults/presets/passkey-first/human-user.json +2 -19
  13. package/defaults/presets/passkey-first/login.json +31 -4
  14. package/dist/branding-url.d.mts +14 -0
  15. package/dist/branding-url.d.mts.map +1 -0
  16. package/dist/branding-url.mjs +23 -0
  17. package/dist/branding-url.mjs.map +1 -0
  18. package/dist/defaults-Bh4_xVbh.d.mts +84 -0
  19. package/dist/defaults-Bh4_xVbh.d.mts.map +1 -0
  20. package/dist/defaults-eiGHyI5Y.mjs +569 -0
  21. package/dist/defaults-eiGHyI5Y.mjs.map +1 -0
  22. package/dist/defaults.d.mts +2 -2
  23. package/dist/defaults.mjs +2 -2
  24. package/dist/index.d.mts +6 -4
  25. package/dist/index.mjs +6 -4
  26. package/dist/{meta-schemas-8nlyVL7g.mjs → meta-schemas-CSc18JqO.mjs} +92 -31
  27. package/dist/meta-schemas-CSc18JqO.mjs.map +1 -0
  28. package/dist/meta-schemas.d.mts +6 -1
  29. package/dist/meta-schemas.d.mts.map +1 -1
  30. package/dist/meta-schemas.mjs +2 -2
  31. package/dist/normalize.d.mts +11 -9
  32. package/dist/normalize.d.mts.map +1 -1
  33. package/dist/normalize.mjs +24 -17
  34. package/dist/normalize.mjs.map +1 -1
  35. package/dist/schemas.d.mts +14 -1
  36. package/dist/schemas.d.mts.map +1 -1
  37. package/dist/schemas.mjs +62 -2
  38. package/dist/schemas.mjs.map +1 -1
  39. package/dist/template.d.mts +76 -0
  40. package/dist/template.d.mts.map +1 -0
  41. package/dist/template.mjs +145 -0
  42. package/dist/template.mjs.map +1 -0
  43. package/dist/validate.mjs +70 -11
  44. package/dist/validate.mjs.map +1 -1
  45. package/meta-schemas/auth-method.json +2 -7
  46. package/meta-schemas/branding.json +44 -0
  47. package/meta-schemas/flow-definition.json +20 -1
  48. package/meta-schemas/property-name.json +7 -0
  49. package/meta-schemas/user-property.json +8 -24
  50. package/meta-schemas/user-schema.json +4 -1
  51. package/package.json +17 -4
  52. package/dist/defaults-BQej3KIY.mjs +0 -388
  53. package/dist/defaults-BQej3KIY.mjs.map +0 -1
  54. package/dist/defaults-CSjBjBkQ.d.mts +0 -41
  55. package/dist/defaults-CSjBjBkQ.d.mts.map +0 -1
  56. package/dist/meta-schemas-8nlyVL7g.mjs.map +0 -1
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @zitadel/config
2
+
3
+ Versioned Zitadel local config schemas and defaults — the contract behind the
4
+ `.zitadel/` directory that `zitadel setup` scaffolds into your project.
5
+
6
+ ## What's in here
7
+
8
+ | Import | What it carries |
9
+ | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
10
+ | `@zitadel/config/schemas` | Zod schemas for the local config files (`schemaConfigSchema`, `flowConfigSchema`, `brandingConfigSchema`) |
11
+ | `@zitadel/config/branding-url` | Canonical loopback HTTP URL predicate shared by config and browser render gates |
12
+ | `@zitadel/config/validate` | Validation entry points the CLI runs on `plan`/`apply` |
13
+ | `@zitadel/config/normalize` | Normalization used to compare local files against server state |
14
+ | `@zitadel/config/defaults` | The versioned default user schema, login flow, and branding designs |
15
+ | `@zitadel/config/meta-schemas` | The JSON meta-schemas the defaults pin |
16
+ | `@zitadel/config/template` | Liquid template helpers |
17
+ | `@zitadel/config/defaults/default-login.json` | The default login flow — the authority for flow step shape |
18
+ | `@zitadel/config/defaults/default-human-user.json` | The default user schema |
19
+
20
+ ## The customer-facing part
21
+
22
+ `zitadel setup` copies the defaults into your repo as
23
+ `.zitadel/{schemas,flows,branding}/`, each with a README explaining how to
24
+ edit it (`defaults/README-schemas.md`, `README-flows.md`,
25
+ `README-branding.md` in this package become those files). Those files are
26
+ yours to edit; `zitadel plan` / `zitadel apply` reconcile them with the
27
+ server, and edits publish new revisions rather than mutating state in place.
@@ -0,0 +1,96 @@
1
+ # Branding
2
+
3
+ This directory owns how your login UI looks: `branding.json` (layout preset
4
+ and asset URLs) plus `login.liquid`, the LiquidJS template the
5
+ `<zitadel-login>` component renders for every step.
6
+
7
+ ## Workflow
8
+
9
+ 1. Edit `login.liquid` (and `branding.json` for logo/hero URLs).
10
+ 2. `zitadel plan` — validates the template (LiquidJS parse, banned patterns,
11
+ the required `{% mandatory_gates %}` tag) and shows the pending revision.
12
+ 3. `zitadel apply` — publishes an immutable branding revision. The login UI
13
+ picks up the newest revision on the next step response.
14
+
15
+ Every edit publishes a new revision; there is no in-place update. Roll back
16
+ by re-applying an earlier template.
17
+
18
+ ## Rules of the template
19
+
20
+ - Compose `<zl-*>` atoms and plain structural HTML. No `<script>`, no
21
+ `<style>`, no inline event handlers — theming happens through design
22
+ tokens, not template CSS.
23
+ - User-facing copy goes through translation keys: `{{ "key" | t }}`.
24
+ - Keep the trailing `{% mandatory_gates %}` tag: it appends any required
25
+ field, gate, or submit action your template forgot, so a broken template
26
+ still yields a submittable step.
27
+
28
+ ## Make it yours
29
+
30
+ - Brand asset URLs go in `branding.json` and must use `https://`. Each template
31
+ decides which fields it renders: `centered` and `hero` use `logo_url`; `split`
32
+ and `split-right` use both `logo_url` and `hero_url`; `minimal` uses neither
33
+ until you add them to `login.liquid`. The `hero` design's brand pane is
34
+ editable markup rather than a `hero_url` background. Custom fonts are not
35
+ configurable here yet; load them from the embedding page.
36
+
37
+ ```json
38
+ {
39
+ "$schema": "../meta/branding.json",
40
+ "layout": "split",
41
+ "liquid_template_file": "./login.liquid",
42
+ "logo_url": "https://example.com/logo.svg",
43
+ "hero_url": "https://example.com/hero.jpg"
44
+ }
45
+ ```
46
+
47
+ A well-formed URL that serves nothing is the one branding mistake nothing
48
+ else catches — it passes validation, publishes a revision, and then renders
49
+ as an invisible image. So `zitadel plan` probes each asset URL and warns
50
+ (never fails) when it is unreachable, bodyless, or not an image, and the
51
+ login UI hides an asset that fails to load, restoring the shipped design's
52
+ no-asset content rather than leaving a gap. If the host is only
53
+ reachable from where the login page renders — or you are offline — set
54
+ `ZITADEL_SKIP_ASSET_PROBE=1` to skip the check. The probe contacts public
55
+ HTTPS destinations only and re-checks redirects; loopback/private/internal
56
+ targets are left to the browser-side fallback instead of being requested by
57
+ the machine running `plan`.
58
+
59
+ `layout` is the degrade preset (`centered` or `split`), **not** the complete
60
+ design catalog. All named designs (`centered`, `split`, `split-right`, `hero`,
61
+ `minimal`) are delivered as templates and map onto those two values. Switch
62
+ designs with `zitadel branding eject --design <name>`, don't edit `layout`.
63
+
64
+ - In the split-family designs (`split`, `split-right`, `hero`) the
65
+ `.zl-split__brand` pane is yours: structural HTML plus inline `style=""`
66
+ attributes are allowed; `button`, `input`, and `form` tags are stripped —
67
+ use `<a>` for landing CTAs. The `hero` design ships a full landing-page
68
+ starting point on token-styled `zl-hero__*` classes.
69
+ - On narrow viewports the brand pane collapses and the `.zl-split__compact`
70
+ node inside the form pane takes over (logo or brand line) — keep one so
71
+ your identity survives on phones.
72
+ - The split chrome is tunable from your template root's `style` attribute
73
+ (`--zl-split-columns`, `--zl-split-align`, `--zl-split-brand-mobile`), and
74
+ `zl-split--right` on the wrapper mirrors the panes. Knob reference:
75
+ [`docs/design/branding/templates.md`](https://github.com/zitadel/nextgen/blob/main/docs/design/branding/templates.md).
76
+ - The "Secured with Zitadel" attribution is licence-gated and on by default.
77
+ - Back-navigation: the engine injects a `kind: "back"` action on steps
78
+ that can return to their predecessor, and the browser's back gesture
79
+ submits it automatically — the shipped designs deliberately render **no
80
+ visible back control**. Your template may render one if you want it;
81
+ select the action by kind (never by name) and submit it like any other
82
+ action:
83
+
84
+ ```liquid
85
+ {% assign back = actions | where: "kind", "back" | first %}
86
+ {% if back %}
87
+ <a href="#" class="zl-card-nav__link" data-action="{{ back.name }}">{{ back.text_key | t }}</a>
88
+ {% endif %}
89
+ ```
90
+
91
+ If you list actions generically, exclude `a.kind == 'back'` from button
92
+ loops so the injected action doesn't render as a stray secondary button.
93
+
94
+ Start over anytime with `zitadel branding eject --design <name>` (designs:
95
+ `centered`, `split`, `split-right`, `hero`, `minimal`; add `--force` to
96
+ overwrite).
@@ -37,8 +37,25 @@ Typical edits:
37
37
  tokens).
38
38
  - **Add or remove a step** — extend `steps[]`.
39
39
  - **Rewire transitions** — edit `steps[].transitions` to point at a
40
- different next step, or use `action: switch` / `pivot` to jump to
41
- another flow.
40
+ different next step. Cross-flow jumps (`action: switch` / `pivot`)
41
+ validate in a definition but are **not yet executed by the runtime** —
42
+ submitting through one fails with a 400 (`code: "flow.unsupported"`);
43
+ keep transitions within one flow, using a purpose switch (next bullet)
44
+ for login ↔ register navigation.
45
+ - **Offer a purpose switch in the card** — pair a `kind: navigate`
46
+ action with a transition that declares a local `purpose`. The default
47
+ login flow ships this in both directions:
48
+
49
+ ```json
50
+ "register": { "target": "register", "purpose": "register" }
51
+ ```
52
+
53
+ Navigation skips field validation (an empty email box can't block the
54
+ link), and the declared purpose is what makes the landing leg run with
55
+ the right semantics — register creates the user instead of trying to
56
+ verify one. Rules: the purpose must be one this flow's `purposes`
57
+ serves, the target must be that purpose's entry step, and `purpose`
58
+ never combines with `action` (which targets another flow instead).
42
59
  - **Add another flow** — drop a new JSON file with its own `purposes`
43
60
  and a distinct `name` (e.g. a per-team login). See
44
61
  [Multiple flows](#multiple-flows) for how it gets selected at runtime.
@@ -70,8 +87,8 @@ experiment can't silently take over `/login` — scope it or pin
70
87
 
71
88
  ## Presets
72
89
 
73
- `zitadel setup` scaffolds this folder from a preset (`--preset
74
- password-first` or `--preset passkey-first`). The passkey-first flow
90
+ `zitadel setup` scaffolds this folder from a preset: `password-first` (the
91
+ default root files) or `--preset passkey-first`. The passkey-first flow
75
92
  enters login on a fields-less passkey step with an email → password
76
93
  fallback path. The preset only decides the starting point — edit
77
94
  anything here afterwards.
@@ -28,8 +28,8 @@ Here's a simplified example:
28
28
  "firstName"
29
29
  ],
30
30
  "x-auth-methods": {
31
- "password": { "enabled": true, "position": 1 },
32
- "passkey": { "enabled": true, "position": 2 }
31
+ "password": { "enabled": true },
32
+ "passkey": { "enabled": true }
33
33
  }
34
34
  }
35
35
  ```
@@ -0,0 +1,164 @@
1
+ <zl-page-shell data-zl-template-root>
2
+ {% if branding.logo_url %}
3
+ <img slot="header" class="zl-card-logo" src="{{ branding.logo_url }}" alt="" />
4
+ {% endif %}
5
+ <zl-card>
6
+ <h1 slot="header" class="zl-card-title">{{ step.texts.title_key | t: identity.display_name }}</h1>
7
+ {% if step.texts.description_key %}
8
+ {% assign card_subtitle = step.texts.description_key | t: identity.display_name %}
9
+ {% if card_subtitle != "" %}
10
+ <p slot="header" class="zl-card-subtitle">{{ card_subtitle }}</p>
11
+ {% endif %}
12
+ {% endif %}
13
+
14
+ {% if errors and errors.size > 0 %}
15
+ {% for err in errors %}
16
+ {% if err | formLevelError %}
17
+ {% assign alert_heading = err.text_key | alertHeading %}
18
+ {% assign alert_body = err.text_key | alertBody %}
19
+ <zl-alert severity="error" dismissible data-zl-step-error{% if alert_heading != "" %} heading="{{ alert_heading | t }}"{% endif %}>
20
+ {% if alert_body != "" %}{{ alert_body | t }}{% elsif err.text_key %}{{ err.text_key | t }}{% else %}{{ err.message }}{% endif %}
21
+ </zl-alert>
22
+ {% endif %}
23
+ {% endfor %}
24
+ {% endif %}
25
+
26
+ {% for f in fields %}
27
+ {% assign field_placeholder = f.text_key | fieldPlaceholder %}
28
+ {% assign field_help = f.text_key | fieldHelp %}
29
+ {% assign field_err = f.name | fieldError: errors %}
30
+ {% case f.type %}
31
+ {% when 'checkbox' %}
32
+ <zl-checkbox
33
+ name="{{ f.name }}"
34
+ data-testid="zitadel-field-{{ f.name | testid }}"
35
+ label="{{ f.text_key | t }}"
36
+ value="true"
37
+ {% if f.value %}checked{% endif %}
38
+ {% if field_err != "" %}invalid error="{{ field_err }}"{% endif %}
39
+ ></zl-checkbox>
40
+ {% when 'select' %}
41
+ <zl-select
42
+ name="{{ f.name }}"
43
+ data-testid="zitadel-field-{{ f.name | testid }}"
44
+ label="{{ f.text_key | t }}"
45
+ value="{{ f.value }}"
46
+ options='{{ f.validation.enum | selectOptions | json }}'
47
+ {% if f.required %}required{% endif %}
48
+ {% if field_placeholder != "" %}placeholder="{{ field_placeholder }}"{% endif %}
49
+ {% if field_err != "" %}invalid error="{{ field_err }}"{% endif %}
50
+ ></zl-select>
51
+ {% else %}
52
+ {% assign autocomplete_val = '' %}
53
+ {% if f.name == 'email' %}
54
+ {% assign autocomplete_val = 'email' %}
55
+ {% elsif f.type == 'password' and step.name contains 'register' %}
56
+ {% assign autocomplete_val = 'new-password' %}
57
+ {% elsif f.type == 'password' %}
58
+ {% assign autocomplete_val = 'current-password' %}
59
+ {% endif %}
60
+ <zl-field
61
+ name="{{ f.name }}"
62
+ data-testid="zitadel-field-{{ f.name | testid }}"
63
+ label="{{ f.text_key | t }}"
64
+ type="{{ f.type }}"
65
+ value="{{ f.value }}"
66
+ {% if autocomplete_val != '' %}autocomplete="{{ autocomplete_val }}"{% endif %}
67
+ {% if f.required %}required{% endif %}
68
+ {% if field_placeholder != "" %}placeholder="{{ field_placeholder }}"{% endif %}
69
+ {% if field_err != "" %}invalid error="{{ field_err }}"{% endif %}
70
+ >
71
+ {% if field_help != "" %}
72
+ <span slot="help">{{ field_help }}</span>
73
+ {% endif %}
74
+ </zl-field>
75
+ {% endcase %}
76
+ {% endfor %}
77
+
78
+ {% assign recover = actions | where: "name", "recover" | first %}
79
+ {% if recover %}
80
+ <p class="zl-card-forgot">
81
+ <a href="#" class="zl-card-forgot__link" data-action="recover">{{ recover.text_key | t }}</a>
82
+ </p>
83
+ {% endif %}
84
+
85
+ {% for a in actions %}
86
+ {% if a.primary %}
87
+ <zl-button
88
+ hierarchy="primary"
89
+ size="medium"
90
+ type="submit"
91
+ block
92
+ action="{{ a.name }}"
93
+ data-testid="zitadel-action-{{ a.name }}"
94
+ label="{{ a.text_key | t }}"
95
+ {% if loading %}loading{% endif %}
96
+ ></zl-button>
97
+ {% endif %}
98
+ {% endfor %}
99
+
100
+ {% for a in actions %}
101
+ {% unless a.primary or a.kind == 'back' or a.name == 'passkey' or a.name == 'register' or a.name == 'sign_in' or a.name == 'recover' %}
102
+ <zl-button
103
+ hierarchy="secondary"
104
+ size="medium"
105
+ type="button"
106
+ block
107
+ action="{{ a.name }}"
108
+ data-testid="zitadel-action-{{ a.name }}"
109
+ label="{{ a.text_key | t }}"
110
+ ></zl-button>
111
+ {% endunless %}
112
+ {% endfor %}
113
+
114
+ {% assign passkey = actions | where: "name", "passkey" | first %}
115
+ {% if passkey %}
116
+ {% unless passkey.primary %}
117
+ <zl-button
118
+ hierarchy="secondary"
119
+ size="medium"
120
+ block
121
+ action="passkey"
122
+ data-testid="zitadel-action-passkey"
123
+ label="{{ passkey.text_key | t }}"
124
+ ></zl-button>
125
+ {% endunless %}
126
+ {% endif %}
127
+
128
+
129
+ {% assign register = actions | where: "name", "register" | first %}
130
+ {% if register %}
131
+ <p class="zl-card-nav">
132
+ {{ 'identifier.action.register.lead' | t }}<a href="#" class="zl-card-nav__link" data-action="register" data-testid="zitadel-action-register-link">{{ 'identifier.action.register.link' | t }}</a>
133
+ </p>
134
+ {% endif %}
135
+
136
+ {% assign sign_in = actions | where: "name", "sign_in" | first %}
137
+ {% if sign_in %}
138
+ <p class="zl-card-nav">
139
+ {{ 'register.action.sign_in.lead' | t }}<a href="#" class="zl-card-nav__link" data-action="sign_in" data-testid="zitadel-action-sign_in-link">{{ 'register.action.sign_in.link' | t }}</a>
140
+ </p>
141
+ {% endif %}
142
+
143
+ {% if challenge %}
144
+ {% if challenge.method == "passkey" or challenge.method == "passkey_register" %}
145
+ {% assign ceremony = "authenticate" %}
146
+ {% assign submit_method = challenge.method %}
147
+ {% if challenge.method == "passkey_register" or challenge.options.user %}
148
+ {% assign ceremony = "register" %}
149
+ {% assign submit_method = "passkey_register" %}
150
+ {% endif %}
151
+ <zl-passkey
152
+ ceremony="{{ ceremony }}"
153
+ method="{{ submit_method }}"
154
+ challenge-id="{{ challenge.challenge_id }}"
155
+ options='{{ challenge.options | json }}'
156
+ pending-label="{{ 'passkey.pending.status' | t }}"
157
+ cancel-label="{{ 'action.cancel' | t }}"
158
+ ></zl-passkey>
159
+ {% endif %}
160
+ {% endif %}
161
+
162
+ {% mandatory_gates %}
163
+ </zl-card>
164
+ </zl-page-shell>
@@ -0,0 +1,224 @@
1
+ <zl-page-shell data-zl-template-root>
2
+ <div class="zl-split">
3
+ <aside class="zl-split__brand">
4
+ {% comment %} ──────────────────────────────────────────────────────────
5
+ YOUR LANDING CONTENT — everything inside this aside is yours.
6
+ Edit the copy and structure freely: structural HTML (div, section,
7
+ nav, h1–h6, ul, li, a, img, …) plus inline style="" attributes are
8
+ allowed; button, input, form, script, and style tags are stripped —
9
+ use plain anchors for landing CTAs. The zl-hero__* classes keep
10
+ this pane on design tokens so palettes and theme mode apply.
11
+
12
+ The login card in the section below is fully wired to the flow
13
+ engine; leave its zl-* atoms intact. Layout knobs (set on the root
14
+ element's style attribute): --zl-split-columns, --zl-split-align,
15
+ --zl-split-brand-mobile. Add zl-split--right to the wrapper div to
16
+ mirror the panes. More in .zitadel/branding/README.md.
17
+ ────────────────────────────────────────────────────────────── {% endcomment %}
18
+ <div class="zl-hero">
19
+ <nav class="zl-hero__nav">
20
+ <span class="zl-hero__brand">
21
+ {% if branding.logo_url %}
22
+ <img class="zl-hero__brand-logo" src="{{ branding.logo_url }}" alt="" data-zl-asset-fallback />
23
+ <span class="zl-hero__brand-mark" aria-hidden="true" hidden data-zl-asset-fallback-content></span>
24
+ {% else %}
25
+ <span class="zl-hero__brand-mark" aria-hidden="true"></span>
26
+ {% endif %}
27
+ Your brand
28
+ </span>
29
+ <span class="zl-hero__nav-links">
30
+ <a href="#">Product</a>
31
+ <a href="#">Pricing</a>
32
+ <a href="#">Docs</a>
33
+ </span>
34
+ </nav>
35
+
36
+ <p class="zl-hero__eyebrow">Your product category</p>
37
+ <h2 class="zl-hero__headline">A headline about what you ship.</h2>
38
+ <p class="zl-hero__sub">
39
+ Use this pane to tell visitors why they sign up: what your product
40
+ does, who it is for, and what happens after they sign in. Replace
41
+ every line here before you publish.
42
+ </p>
43
+
44
+ <ul class="zl-hero__bullets">
45
+ <li>Your first selling point</li>
46
+ <li>Your second selling point</li>
47
+ <li>Your third selling point</li>
48
+ </ul>
49
+
50
+ <p class="zl-hero__footnote">
51
+ Replace this line with real social proof, such as customer logos or
52
+ a certification you actually hold.
53
+ </p>
54
+ </div>
55
+ </aside>
56
+ <section class="zl-split__form">
57
+ {% if branding.logo_url %}
58
+ <img class="zl-split__compact" src="{{ branding.logo_url }}" alt="" data-zl-asset-fallback />
59
+ <p class="zl-split__compact zl-hero__compact-brand" hidden data-zl-asset-fallback-content>Your brand</p>
60
+ {% else %}
61
+ <p class="zl-split__compact zl-hero__compact-brand">Your brand</p>
62
+ {% endif %}
63
+ <zl-card>
64
+ <h1 slot="header" class="zl-card-title">{{ step.texts.title_key | t: identity.display_name }}</h1>
65
+ {% if step.texts.description_key %}
66
+ {% assign card_subtitle = step.texts.description_key | t: identity.display_name %}
67
+ {% if card_subtitle != "" %}
68
+ <p slot="header" class="zl-card-subtitle">{{ card_subtitle }}</p>
69
+ {% endif %}
70
+ {% endif %}
71
+
72
+ {% if errors and errors.size > 0 %}
73
+ {% for err in errors %}
74
+ {% if err | formLevelError %}
75
+ {% assign alert_heading = err.text_key | alertHeading %}
76
+ {% assign alert_body = err.text_key | alertBody %}
77
+ <zl-alert severity="error" dismissible data-zl-step-error{% if alert_heading != "" %} heading="{{ alert_heading | t }}"{% endif %}>
78
+ {% if alert_body != "" %}{{ alert_body | t }}{% elsif err.text_key %}{{ err.text_key | t }}{% else %}{{ err.message }}{% endif %}
79
+ </zl-alert>
80
+ {% endif %}
81
+ {% endfor %}
82
+ {% endif %}
83
+
84
+ {% for f in fields %}
85
+ {% assign field_placeholder = f.text_key | fieldPlaceholder %}
86
+ {% assign field_help = f.text_key | fieldHelp %}
87
+ {% assign field_err = f.name | fieldError: errors %}
88
+ {% case f.type %}
89
+ {% when 'checkbox' %}
90
+ <zl-checkbox
91
+ name="{{ f.name }}"
92
+ data-testid="zitadel-field-{{ f.name | testid }}"
93
+ label="{{ f.text_key | t }}"
94
+ value="true"
95
+ {% if f.value %}checked{% endif %}
96
+ {% if field_err != "" %}invalid error="{{ field_err }}"{% endif %}
97
+ ></zl-checkbox>
98
+ {% when 'select' %}
99
+ <zl-select
100
+ name="{{ f.name }}"
101
+ data-testid="zitadel-field-{{ f.name | testid }}"
102
+ label="{{ f.text_key | t }}"
103
+ value="{{ f.value }}"
104
+ options='{{ f.validation.enum | selectOptions | json }}'
105
+ {% if f.required %}required{% endif %}
106
+ {% if field_placeholder != "" %}placeholder="{{ field_placeholder }}"{% endif %}
107
+ {% if field_err != "" %}invalid error="{{ field_err }}"{% endif %}
108
+ ></zl-select>
109
+ {% else %}
110
+ {% assign autocomplete_val = '' %}
111
+ {% if f.name == 'email' %}
112
+ {% assign autocomplete_val = 'email' %}
113
+ {% elsif f.type == 'password' and step.name contains 'register' %}
114
+ {% assign autocomplete_val = 'new-password' %}
115
+ {% elsif f.type == 'password' %}
116
+ {% assign autocomplete_val = 'current-password' %}
117
+ {% endif %}
118
+ <zl-field
119
+ name="{{ f.name }}"
120
+ data-testid="zitadel-field-{{ f.name | testid }}"
121
+ label="{{ f.text_key | t }}"
122
+ type="{{ f.type }}"
123
+ value="{{ f.value }}"
124
+ {% if autocomplete_val != '' %}autocomplete="{{ autocomplete_val }}"{% endif %}
125
+ {% if f.required %}required{% endif %}
126
+ {% if field_placeholder != "" %}placeholder="{{ field_placeholder }}"{% endif %}
127
+ {% if field_err != "" %}invalid error="{{ field_err }}"{% endif %}
128
+ >
129
+ {% if field_help != "" %}
130
+ <span slot="help">{{ field_help }}</span>
131
+ {% endif %}
132
+ </zl-field>
133
+ {% endcase %}
134
+ {% endfor %}
135
+
136
+ {% assign recover = actions | where: "name", "recover" | first %}
137
+ {% if recover %}
138
+ <p class="zl-card-forgot">
139
+ <a href="#" class="zl-card-forgot__link" data-action="recover">{{ recover.text_key | t }}</a>
140
+ </p>
141
+ {% endif %}
142
+
143
+ {% for a in actions %}
144
+ {% if a.primary %}
145
+ <zl-button
146
+ hierarchy="primary"
147
+ size="medium"
148
+ type="submit"
149
+ block
150
+ action="{{ a.name }}"
151
+ data-testid="zitadel-action-{{ a.name }}"
152
+ label="{{ a.text_key | t }}"
153
+ {% if loading %}loading{% endif %}
154
+ ></zl-button>
155
+ {% endif %}
156
+ {% endfor %}
157
+
158
+ {% for a in actions %}
159
+ {% unless a.primary or a.kind == 'back' or a.name == 'passkey' or a.name == 'register' or a.name == 'sign_in' or a.name == 'recover' %}
160
+ <zl-button
161
+ hierarchy="secondary"
162
+ size="medium"
163
+ type="button"
164
+ block
165
+ action="{{ a.name }}"
166
+ data-testid="zitadel-action-{{ a.name }}"
167
+ label="{{ a.text_key | t }}"
168
+ ></zl-button>
169
+ {% endunless %}
170
+ {% endfor %}
171
+
172
+ {% assign passkey = actions | where: "name", "passkey" | first %}
173
+ {% if passkey %}
174
+ {% unless passkey.primary %}
175
+ <zl-button
176
+ hierarchy="secondary"
177
+ size="medium"
178
+ block
179
+ action="passkey"
180
+ data-testid="zitadel-action-passkey"
181
+ label="{{ passkey.text_key | t }}"
182
+ ></zl-button>
183
+ {% endunless %}
184
+ {% endif %}
185
+
186
+
187
+ {% assign register = actions | where: "name", "register" | first %}
188
+ {% if register %}
189
+ <p class="zl-card-nav">
190
+ {{ 'identifier.action.register.lead' | t }}<a href="#" class="zl-card-nav__link" data-action="register" data-testid="zitadel-action-register-link">{{ 'identifier.action.register.link' | t }}</a>
191
+ </p>
192
+ {% endif %}
193
+
194
+ {% assign sign_in = actions | where: "name", "sign_in" | first %}
195
+ {% if sign_in %}
196
+ <p class="zl-card-nav">
197
+ {{ 'register.action.sign_in.lead' | t }}<a href="#" class="zl-card-nav__link" data-action="sign_in" data-testid="zitadel-action-sign_in-link">{{ 'register.action.sign_in.link' | t }}</a>
198
+ </p>
199
+ {% endif %}
200
+
201
+ {% if challenge %}
202
+ {% if challenge.method == "passkey" or challenge.method == "passkey_register" %}
203
+ {% assign ceremony = "authenticate" %}
204
+ {% assign submit_method = challenge.method %}
205
+ {% if challenge.method == "passkey_register" or challenge.options.user %}
206
+ {% assign ceremony = "register" %}
207
+ {% assign submit_method = "passkey_register" %}
208
+ {% endif %}
209
+ <zl-passkey
210
+ ceremony="{{ ceremony }}"
211
+ method="{{ submit_method }}"
212
+ challenge-id="{{ challenge.challenge_id }}"
213
+ options='{{ challenge.options | json }}'
214
+ pending-label="{{ 'passkey.pending.status' | t }}"
215
+ cancel-label="{{ 'action.cancel' | t }}"
216
+ ></zl-passkey>
217
+ {% endif %}
218
+ {% endif %}
219
+
220
+ {% mandatory_gates %}
221
+ </zl-card>
222
+ </section>
223
+ </div>
224
+ </zl-page-shell>