@zitadel/components 0.1.0-alpha.15 → 0.1.0-alpha.17

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 (33) hide show
  1. package/README.md +5 -1
  2. package/dist/atoms/index.d.mts +1 -1
  3. package/dist/atoms/index.mjs +1 -1
  4. package/dist/{atoms-BSwVl736.mjs → atoms-KC7OJWXZ.mjs} +138 -12
  5. package/dist/atoms-KC7OJWXZ.mjs.map +1 -0
  6. package/dist/default-CfF7GoLv.mjs +6 -0
  7. package/dist/default-CfF7GoLv.mjs.map +1 -0
  8. package/dist/{index-EC-BnGsx.d.mts → index-BniwXaIC.d.mts} +3 -1
  9. package/dist/{index-EC-BnGsx.d.mts.map → index-BniwXaIC.d.mts.map} +1 -1
  10. package/dist/{index-Bypr00UG.d.mts → index-DSZnfOJH.d.mts} +368 -17
  11. package/dist/index-DSZnfOJH.d.mts.map +1 -0
  12. package/dist/{index-BP3l3mje.d.mts → index-DjYJwejg.d.mts} +51 -4
  13. package/dist/index-DjYJwejg.d.mts.map +1 -0
  14. package/dist/index.d.mts +4 -4
  15. package/dist/index.mjs +4 -4
  16. package/dist/manifests.mjs +1 -1
  17. package/dist/orchestrator/index.d.mts +2 -2
  18. package/dist/orchestrator/index.mjs +2 -2
  19. package/dist/{orchestrator-Ty4uM5n_.mjs → orchestrator-BWqJw4Yl.mjs} +348 -25
  20. package/dist/orchestrator-BWqJw4Yl.mjs.map +1 -0
  21. package/dist/standalone.mjs +485 -34
  22. package/dist/tokens/index.d.mts +1 -1
  23. package/dist/tokens/index.mjs +1 -1
  24. package/dist/{tokens-C0j4iCrs.mjs → tokens-CbH2s1jV.mjs} +6 -4
  25. package/dist/tokens-CbH2s1jV.mjs.map +1 -0
  26. package/package.json +3 -2
  27. package/dist/atoms-BSwVl736.mjs.map +0 -1
  28. package/dist/default-BmX2ZEbg.mjs +0 -6
  29. package/dist/default-BmX2ZEbg.mjs.map +0 -1
  30. package/dist/index-BP3l3mje.d.mts.map +0 -1
  31. package/dist/index-Bypr00UG.d.mts.map +0 -1
  32. package/dist/orchestrator-Ty4uM5n_.mjs.map +0 -1
  33. package/dist/tokens-C0j4iCrs.mjs.map +0 -1
@@ -823,7 +823,8 @@ const tokens = {
823
823
  "container": {
824
824
  "authCard": "24rem",
825
825
  "page": "80rem"
826
- }
826
+ },
827
+ "layout": {}
827
828
  };
828
829
  const cssVars = {
829
830
  "color": {
@@ -942,9 +943,10 @@ const cssVars = {
942
943
  "container": {
943
944
  "authCard": "var(--zl-container-auth-card)",
944
945
  "page": "var(--zl-container-page)"
945
- }
946
+ },
947
+ "layout": {}
946
948
  };
947
- const tokensCss = "/* AUTOGENERATED by packages/design-tokens/scripts/build.ts — do not edit by hand.\n Source: src/generated/figma.tokens.json + src/overrides.ts.\n Re-run: moon run design-tokens:generate */\n\n:root,\n[data-theme=\"dark\"] {\n --zl-color-surface-default-black: #0f0f11;\n --zl-color-surface-default-white: #f4f4f6;\n --zl-color-surface-default-primary-gray: #252528;\n --zl-color-surface-default-secondary-gray: #37373a;\n --zl-color-text-primary-white: #f4f4f6;\n --zl-color-text-secondary-gray: #bfbfcf;\n --zl-color-text-button-default: #0f0f11;\n --zl-color-text-button-invert: #f4f4f6;\n --zl-color-text-disabled: #686883;\n --zl-color-text-error: #ea4f70;\n --zl-color-text-success: #33a779;\n --zl-color-text-subtitle-pink: #f48aa0;\n --zl-color-text-subtitle-purple: #bba5e4;\n --zl-color-text-subtitle-gray: #bfbfcf;\n --zl-color-text-subtitle-orange: #f25543;\n --zl-color-icon-default-black: #0f0f11;\n --zl-color-icon-default-white: #f4f4f6;\n --zl-color-icon-default-gray: #bfbfcf;\n --zl-color-icon-default-pink: #f48aa0;\n --zl-color-icon-default-purple: #bba5e4;\n --zl-color-icon-default-orange: #f25543;\n --zl-color-icon-disabled: #686883;\n --zl-color-icon-error: #ea4f70;\n --zl-color-icon-success: #33a779;\n --zl-color-border-default-black: #252528;\n --zl-color-border-default-white: #f4f4f6;\n --zl-color-border-default-gray-100: #37373a;\n --zl-color-border-default-gray-200: #484a57;\n --zl-color-border-default-gray-300: #686883;\n --zl-color-border-default-gray-400: #82829c;\n --zl-color-border-error: #ea4f70;\n --zl-color-border-success: #33a779;\n --zl-color-gray-50: #0f0f11;\n --zl-color-gray-75: #252528;\n --zl-color-gray-100: #37373a;\n --zl-color-gray-200: #484a57;\n --zl-color-gray-300: #686883;\n --zl-color-gray-400: #82829c;\n --zl-color-gray-500: #9e9eb2;\n --zl-color-gray-600: #cfcfde;\n --zl-color-gray-700: #bfbfcf;\n --zl-color-gray-800: #e1e1e7;\n --zl-color-gray-900: #f4f4f6;\n --zl-spacing-01: 0.25rem;\n --zl-spacing-02: 0.5rem;\n --zl-spacing-03: 1rem;\n --zl-spacing-04: 1.5rem;\n --zl-spacing-05: 2rem;\n --zl-spacing-06: 1.5rem;\n --zl-spacing-07: 2.5rem;\n --zl-spacing-08: 3rem;\n --zl-spacing-09: 3.5rem;\n --zl-spacing-10: 4rem;\n --zl-spacing-11: 5rem;\n --zl-spacing-12: 5.5rem;\n --zl-spacing-13: 6rem;\n --zl-spacing-14: 6.5rem;\n --zl-spacing-15: 7rem;\n --zl-spacing-16: 7.5rem;\n --zl-radius-xs: 0.25rem;\n --zl-radius-s: 0.5rem;\n --zl-radius-m: 0.75rem;\n --zl-radius-l: 1rem;\n --zl-radius-xl: 1.5rem;\n --zl-font-family-sans: \"Arimo\", system-ui, -apple-system, \"Segoe UI\", Helvetica, Arial, sans-serif;\n --zl-font-family-heading: \"Arimo\", system-ui, -apple-system, \"Segoe UI\", Helvetica, Arial, sans-serif;\n --zl-font-family-mono: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n --zl-duration-instant: 0ms;\n --zl-duration-fast: 120ms;\n --zl-duration-base: 200ms;\n --zl-duration-slow: 320ms;\n --zl-easing-standard: cubic-bezier(0.2, 0, 0, 1);\n --zl-easing-decelerate: cubic-bezier(0, 0, 0, 1);\n --zl-easing-accelerate: cubic-bezier(0.3, 0, 1, 1);\n --zl-focus-width: 2px;\n --zl-focus-offset: 2px;\n --zl-focus-color: #f4f4f6;\n --zl-breakpoint-xs: 26.5625rem;\n --zl-breakpoint-sm: 40rem;\n --zl-breakpoint-md: 48rem;\n --zl-breakpoint-lg: 64rem;\n --zl-breakpoint-xl: 80rem;\n --zl-breakpoint-2xl: 96rem;\n --zl-breakpoint-3xl: 120rem;\n --zl-breakpoint-4xl: 160rem;\n --zl-container-auth-card: 24rem;\n --zl-container-page: 80rem;\n}\n\n[data-theme=\"light\"] {\n /* Reserved. Light mode lands when the Figma design system publishes\n a light variable mode; the next sync will populate this block. */\n}\n";
949
+ const tokensCss = "/* AUTOGENERATED by packages/design-tokens/scripts/build.ts — do not edit by hand.\n Source: src/generated/figma.tokens.json + src/overrides.ts.\n Re-run: moon run design-tokens:generate */\n\n:root,\n[data-theme=\"dark\"] {\n --zl-color-surface-default-black: #0f0f11;\n --zl-color-surface-default-white: #f4f4f6;\n --zl-color-surface-default-primary-gray: #252528;\n --zl-color-surface-default-secondary-gray: #37373a;\n --zl-color-text-primary-white: #f4f4f6;\n --zl-color-text-secondary-gray: #bfbfcf;\n --zl-color-text-button-default: #0f0f11;\n --zl-color-text-button-invert: #f4f4f6;\n --zl-color-text-disabled: #686883;\n --zl-color-text-error: #ea4f70;\n --zl-color-text-success: #33a779;\n --zl-color-text-subtitle-pink: #f48aa0;\n --zl-color-text-subtitle-purple: #bba5e4;\n --zl-color-text-subtitle-gray: #bfbfcf;\n --zl-color-text-subtitle-orange: #f25543;\n --zl-color-icon-default-black: #0f0f11;\n --zl-color-icon-default-white: #f4f4f6;\n --zl-color-icon-default-gray: #bfbfcf;\n --zl-color-icon-default-pink: #f48aa0;\n --zl-color-icon-default-purple: #bba5e4;\n --zl-color-icon-default-orange: #f25543;\n --zl-color-icon-disabled: #686883;\n --zl-color-icon-error: #ea4f70;\n --zl-color-icon-success: #33a779;\n --zl-color-border-default-black: #252528;\n --zl-color-border-default-white: #f4f4f6;\n --zl-color-border-default-gray-100: #37373a;\n --zl-color-border-default-gray-200: #484a57;\n --zl-color-border-default-gray-300: #686883;\n --zl-color-border-default-gray-400: #82829c;\n --zl-color-border-error: #ea4f70;\n --zl-color-border-success: #33a779;\n --zl-color-gray-50: #0f0f11;\n --zl-color-gray-75: #252528;\n --zl-color-gray-100: #37373a;\n --zl-color-gray-200: #484a57;\n --zl-color-gray-300: #686883;\n --zl-color-gray-400: #82829c;\n --zl-color-gray-500: #9e9eb2;\n --zl-color-gray-600: #cfcfde;\n --zl-color-gray-700: #bfbfcf;\n --zl-color-gray-800: #e1e1e7;\n --zl-color-gray-900: #f4f4f6;\n --zl-spacing-01: 0.25rem;\n --zl-spacing-02: 0.5rem;\n --zl-spacing-03: 1rem;\n --zl-spacing-04: 1.5rem;\n --zl-spacing-05: 2rem;\n --zl-spacing-06: 1.5rem;\n --zl-spacing-07: 2.5rem;\n --zl-spacing-08: 3rem;\n --zl-spacing-09: 3.5rem;\n --zl-spacing-10: 4rem;\n --zl-spacing-11: 5rem;\n --zl-spacing-12: 5.5rem;\n --zl-spacing-13: 6rem;\n --zl-spacing-14: 6.5rem;\n --zl-spacing-15: 7rem;\n --zl-spacing-16: 7.5rem;\n --zl-radius-xs: 0.25rem;\n --zl-radius-s: 0.5rem;\n --zl-radius-m: 0.75rem;\n --zl-radius-l: 1rem;\n --zl-radius-xl: 1.5rem;\n --zl-font-family-sans: \"Arimo\", system-ui, -apple-system, \"Segoe UI\", Helvetica, Arial, sans-serif;\n --zl-font-family-heading: \"Arimo\", system-ui, -apple-system, \"Segoe UI\", Helvetica, Arial, sans-serif;\n --zl-font-family-mono: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n --zl-duration-instant: 0ms;\n --zl-duration-fast: 120ms;\n --zl-duration-base: 200ms;\n --zl-duration-slow: 320ms;\n --zl-easing-standard: cubic-bezier(0.2, 0, 0, 1);\n --zl-easing-decelerate: cubic-bezier(0, 0, 0, 1);\n --zl-easing-accelerate: cubic-bezier(0.3, 0, 1, 1);\n --zl-focus-width: 2px;\n --zl-focus-offset: 2px;\n --zl-focus-color: #f4f4f6;\n --zl-breakpoint-xs: 26.5625rem;\n --zl-breakpoint-sm: 40rem;\n --zl-breakpoint-md: 48rem;\n --zl-breakpoint-lg: 64rem;\n --zl-breakpoint-xl: 80rem;\n --zl-breakpoint-2xl: 96rem;\n --zl-breakpoint-3xl: 120rem;\n --zl-breakpoint-4xl: 160rem;\n --zl-container-auth-card: 24rem;\n --zl-container-page: 80rem;\n}\n\n[data-theme=\"light\"] {\n}\n";
948
950
  //#endregion
949
951
  //#region src/styles/tokens.ts
950
952
  function wrap(node) {
@@ -2056,6 +2058,36 @@ var text_field_host_default = ":host {\n display: block;\n}\n\n::slotted([slot=
2056
2058
  //#region ../shared-component-styles/src/text-field.css?inline
2057
2059
  var text_field_default = ".zr-field {\n align-items: stretch;\n gap: var(--zl-spacing-02, .5rem);\n font-family: var(--zl-font-family-sans, system-ui, sans-serif);\n flex-direction: column;\n display: flex;\n}\n\n.zr-field__label-row {\n justify-content: space-between;\n align-items: flex-start;\n gap: var(--zl-spacing-02, .5rem);\n width: 100%;\n display: flex;\n}\n\n.zr-field__label {\n align-items: center;\n gap: var(--zl-spacing-01, .25rem);\n color: var(--zl-color-text-primary-white, #f4f4f6);\n font-size: 1rem;\n font-weight: 600;\n line-height: 1.5rem;\n display: inline-flex;\n}\n\n.zr-field__link {\n color: var(--zl-color-icon-default-purple, #bba5e4);\n flex-shrink: 0;\n font-size: .875rem;\n font-weight: 400;\n line-height: 1.25rem;\n text-decoration: none;\n}\n\n.zr-field__link:hover {\n text-decoration: underline;\n}\n\n.zr-field__link:focus-visible {\n outline: var(--zl-focus-width, 2px) solid var(--zl-focus-color, #f4f4f6);\n outline-offset: var(--zl-focus-offset, 2px);\n border-radius: 2px;\n}\n\n.zr-field__required {\n color: var(--zl-color-text-error, #ea4f70);\n}\n\n.zr-field--disabled .zr-field__label {\n color: var(--zl-color-text-disabled, #686883);\n}\n\n.zr-field__wrap {\n align-items: center;\n gap: var(--zl-spacing-02, .5rem);\n padding-block: var(--zl-spacing-02, .5rem);\n padding-inline: var(--zl-spacing-03, 1rem);\n border-radius: var(--zl-radius-xs, .25rem);\n border: 1px solid var(--zl-color-border-default-gray-300, #686883);\n height: 2.5rem;\n transition: border-color var(--zl-duration-fast, .12s) var(--zl-easing-standard, cubic-bezier(.2, 0, 0, 1)),\n background-color var(--zl-duration-fast, .12s) var(--zl-easing-standard, cubic-bezier(.2, 0, 0, 1));\n box-sizing: border-box;\n background: none;\n display: flex;\n position: relative;\n overflow: visible;\n}\n\n.zr-field__wrap--trailing {\n padding-inline-end: 2.5rem;\n}\n\n.zr-field:not(.zr-field--disabled) .zr-field__wrap:hover:not(:focus-within) {\n border-color: var(--zl-color-gray-500, #9e9eb2);\n background-color: color-mix(in srgb, var(--zl-color-text-primary-white, #f4f4f6) 10%, transparent);\n}\n\n.zr-field:not(.zr-field--invalid):not(.zr-field--disabled):not(.zr-field--success) .zr-field__wrap:has(.zr-field__input:not(:placeholder-shown)):not(:focus-within) {\n border-color: var(--zl-color-border-default-gray-400, #82829c);\n}\n\n.zr-field:not(.zr-field--invalid):not(.zr-field--disabled):not(.zr-field--success) .zr-field__wrap:focus-within {\n border-color: var(--zl-color-border-default-white, #f4f4f6);\n outline: none;\n}\n\n.zr-field--invalid .zr-field__wrap {\n border-color: var(--zl-color-border-error, #ea4f70);\n}\n\n.zr-field--invalid .zr-field__wrap:focus-within {\n border-color: var(--zl-color-border-error, #ea4f70);\n outline: none;\n}\n\n.zr-field--success .zr-field__wrap {\n border-color: var(--zl-color-border-success, #33a779);\n}\n\n.zr-field--success .zr-field__wrap:focus-within {\n border-color: var(--zl-color-border-success, #33a779);\n outline: none;\n}\n\n.zr-field--disabled .zr-field__wrap {\n border-color: var(--zl-color-border-default-gray-200, #484a57);\n}\n\n.zr-field__input {\n min-width: 0;\n height: 100%;\n color: var(--zl-color-text-primary-white, #f4f4f6);\n background: none;\n border: 0;\n outline: none;\n flex: auto;\n padding: 0;\n font-family: inherit;\n font-size: .875rem;\n font-weight: 400;\n line-height: 1.25rem;\n}\n\n.zr-field__input[type=\"password\"] {\n letter-spacing: .125rem;\n}\n\n.zr-field__input::placeholder {\n color: var(--zl-color-text-secondary-gray, #bfbfcf);\n}\n\n.zr-field__input:focus, .zr-field__input:focus-visible {\n outline: none;\n}\n\n.zr-field__input:disabled {\n cursor: not-allowed;\n color: var(--zl-color-text-disabled, #686883);\n}\n\n.zr-field--disabled .zr-field__wrap:hover {\n border-color: var(--zl-color-border-default-gray-200, #484a57);\n background-color: #0000;\n}\n\n.zr-field__trailing {\n width: 1rem;\n height: 1rem;\n color: var(--zl-color-text-secondary-gray, #bfbfcf);\n justify-content: center;\n align-items: center;\n display: inline-flex;\n position: absolute;\n top: 50%;\n right: .9375rem;\n transform: translateY(-50%);\n}\n\n.zr-field__trailing-action {\n color: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n justify-content: center;\n align-items: center;\n margin: 0;\n padding: 0;\n line-height: 0;\n display: inline-flex;\n}\n\n.zr-field__trailing-action:focus-visible {\n outline: var(--zl-focus-width, 2px) solid var(--zl-focus-color, #f4f4f6);\n outline-offset: var(--zl-focus-offset, 2px);\n border-radius: 2px;\n}\n\n.zr-field__trailing-action:disabled {\n cursor: not-allowed;\n}\n\n.zr-field--invalid .zr-field__trailing {\n color: var(--zl-color-text-error, #ea4f70);\n}\n\n.zr-field--success .zr-field__trailing {\n color: var(--zl-color-text-success, #33a779);\n}\n\n.zr-field__help {\n color: var(--zl-color-text-primary-white, #f4f4f6);\n font-size: .75rem;\n line-height: 1rem;\n}\n\n.zr-field__success {\n color: var(--zl-color-text-success, #33a779);\n font-size: .75rem;\n line-height: 1rem;\n}\n\n.zr-field__error {\n color: var(--zl-color-text-error, #ea4f70);\n font-size: .75rem;\n line-height: 1rem;\n}\n";
2058
2060
  //#endregion
2061
+ //#region src/internal/hook-name.ts
2062
+ /**
2063
+ * Reserved auth-method credential prefix of flow field names. Mirrors
2064
+ * `AUTH_METHOD_PREFIX` in `@zitadel/config` (`src/validate.ts`), which
2065
+ * owns the wire dialect; components has no dependency on that package,
2066
+ * so the literal is repeated here.
2067
+ */
2068
+ const AUTH_METHOD_PREFIX = "x-auth-methods#";
2069
+ /**
2070
+ * Normalises a flow field name into its automation-hook token.
2071
+ *
2072
+ * Auth-method credential fields arrive from the flow engine as
2073
+ * `x-auth-methods#<method>` (e.g. `x-auth-methods#password`), but the
2074
+ * documented `data-testid` hooks are method-named
2075
+ * (`zitadel-field-password`, `zitadel-input-password` — see
2076
+ * packages/components/README.md and apps/cli/SKILLS.md). This helper
2077
+ * feeds both hook construction sites: the bundled template's `testid`
2078
+ * Liquid filter and `<zl-field>`'s native-input testid.
2079
+ *
2080
+ * Only the reserved credential prefix is stripped — user-schema
2081
+ * property names are tenant-authored arbitrary strings, so a `#`
2082
+ * anywhere else is part of the name and stays in the hook. The
2083
+ * after-prefix token is used verbatim (no case or spacing changes),
2084
+ * and the field's `name` attribute always keeps the raw wire/form key.
2085
+ */
2086
+ function hookName(field) {
2087
+ if (field.startsWith(AUTH_METHOD_PREFIX) && field.length > 15) return field.slice(15);
2088
+ return field;
2089
+ }
2090
+ //#endregion
2059
2091
  //#region src/atoms/zl-field.ts
2060
2092
  let ZlField = class ZlField extends i$2 {
2061
2093
  static formAssociated = true;
@@ -2395,7 +2427,7 @@ let ZlField = class ZlField extends i$2 {
2395
2427
  }
2396
2428
  };
2397
2429
  nativeInputTestId() {
2398
- if (this.name) return `zitadel-input-${this.name}`;
2430
+ if (this.name) return `zitadel-input-${hookName(this.name)}`;
2399
2431
  if (this.testId) return `${this.testId}-input`;
2400
2432
  }
2401
2433
  dispatchNativeInput(source) {
@@ -2583,6 +2615,13 @@ function base64UrlToBuffer(base64url) {
2583
2615
  }
2584
2616
  //#endregion
2585
2617
  //#region src/atoms/zl-passkey.ts
2618
+ /**
2619
+ * Slack subtracted from `options.timeout` when classifying a
2620
+ * `NotAllowedError` as a timeout: browsers clamp and fire the deadline
2621
+ * slightly early, so an exact comparison would label real timeouts as
2622
+ * user cancellations.
2623
+ */
2624
+ const TIMEOUT_GRACE_MS = 1e3;
2586
2625
  let ZlPasskey = class ZlPasskey extends i$2 {
2587
2626
  #_ceremony_accessor_storage = "authenticate";
2588
2627
  get ceremony() {
@@ -2619,7 +2658,37 @@ let ZlPasskey = class ZlPasskey extends i$2 {
2619
2658
  set manual(value) {
2620
2659
  this.#_manual_accessor_storage = value;
2621
2660
  }
2661
+ #_pendingLabel_accessor_storage = "Waiting for your passkey…";
2662
+ get pendingLabel() {
2663
+ return this.#_pendingLabel_accessor_storage;
2664
+ }
2665
+ set pendingLabel(value) {
2666
+ this.#_pendingLabel_accessor_storage = value;
2667
+ }
2668
+ #_cancelLabel_accessor_storage = "Cancel";
2669
+ get cancelLabel() {
2670
+ return this.#_cancelLabel_accessor_storage;
2671
+ }
2672
+ set cancelLabel(value) {
2673
+ this.#_cancelLabel_accessor_storage = value;
2674
+ }
2675
+ #_silent_accessor_storage = false;
2676
+ get silent() {
2677
+ return this.#_silent_accessor_storage;
2678
+ }
2679
+ set silent(value) {
2680
+ this.#_silent_accessor_storage = value;
2681
+ }
2682
+ #_pending_accessor_storage = false;
2683
+ get pending() {
2684
+ return this.#_pending_accessor_storage;
2685
+ }
2686
+ set pending(value) {
2687
+ this.#_pending_accessor_storage = value;
2688
+ }
2622
2689
  abortController = null;
2690
+ /** `Date.now()` at ceremony start, for timeout classification on reject. */
2691
+ startedAt = 0;
2623
2692
  connectedCallback() {
2624
2693
  super.connectedCallback();
2625
2694
  if (!this.manual && this.options) this.startCeremony();
@@ -2630,11 +2699,14 @@ let ZlPasskey = class ZlPasskey extends i$2 {
2630
2699
  }
2631
2700
  /**
2632
2701
  * Abort any in-flight WebAuthn ceremony. Called automatically when the
2633
- * component disconnects (step change, page navigation).
2702
+ * component disconnects (step change, page navigation) and by the
2703
+ * pending UI's cancel button. Clears `pending` immediately — the
2704
+ * ceremony's own `finally` no longer owns it (its controller is gone).
2634
2705
  */
2635
2706
  abort() {
2636
2707
  this.abortController?.abort();
2637
2708
  this.abortController = null;
2709
+ this.pending = false;
2638
2710
  }
2639
2711
  /**
2640
2712
  * Trigger the WebAuthn ceremony. Resolves when the ceremony completes
@@ -2650,7 +2722,11 @@ let ZlPasskey = class ZlPasskey extends i$2 {
2650
2722
  return;
2651
2723
  }
2652
2724
  this.abort();
2653
- this.abortController = new AbortController();
2725
+ const controller = new AbortController();
2726
+ this.abortController = controller;
2727
+ this.pending = true;
2728
+ this.startedAt = Date.now();
2729
+ this.emitStarted();
2654
2730
  try {
2655
2731
  const credential = this.ceremony === "register" ? await this.createCredential() : await this.getCredential();
2656
2732
  if (!credential) {
@@ -2661,9 +2737,33 @@ let ZlPasskey = class ZlPasskey extends i$2 {
2661
2737
  this.emitResult(proof);
2662
2738
  } catch (error) {
2663
2739
  const aborted = error instanceof DOMException && (error.name === "AbortError" || error.name === "NotAllowedError");
2664
- this.emitError(error instanceof Error ? error.message : "WebAuthn ceremony failed.", aborted);
2740
+ const timeoutMs = typeof this.options?.timeout === "number" ? this.options.timeout : null;
2741
+ const timedOut = error instanceof DOMException && error.name === "NotAllowedError" && timeoutMs !== null && Date.now() - this.startedAt >= timeoutMs - TIMEOUT_GRACE_MS;
2742
+ this.emitError(error instanceof Error ? error.message : "WebAuthn ceremony failed.", aborted, timedOut);
2743
+ } finally {
2744
+ if (this.abortController === controller) this.pending = false;
2665
2745
  }
2666
2746
  }
2747
+ handleCancelSubmit = (event) => {
2748
+ event.stopPropagation();
2749
+ this.abort();
2750
+ };
2751
+ render() {
2752
+ if (!this.pending || this.silent) return A;
2753
+ return b`
2754
+ <div class="zl-passkey-pending" data-testid="zitadel-passkey-pending">
2755
+ <zl-alert severity="info">${this.pendingLabel}</zl-alert>
2756
+ <zl-button
2757
+ hierarchy="secondary"
2758
+ type="button"
2759
+ block
2760
+ data-testid="zitadel-passkey-cancel"
2761
+ label=${this.cancelLabel}
2762
+ @zl-submit=${this.handleCancelSubmit}
2763
+ ></zl-button>
2764
+ </div>
2765
+ `;
2766
+ }
2667
2767
  /**
2668
2768
  * `navigator.credentials.get()` — authentication ceremony.
2669
2769
  */
@@ -2765,11 +2865,12 @@ let ZlPasskey = class ZlPasskey extends i$2 {
2765
2865
  detail
2766
2866
  }));
2767
2867
  }
2768
- emitError(message, aborted) {
2868
+ emitError(message, aborted, timedOut = false) {
2769
2869
  const detail = {
2770
2870
  challenge_id: this.challengeId,
2771
2871
  error: message,
2772
- aborted
2872
+ aborted,
2873
+ timed_out: timedOut
2773
2874
  };
2774
2875
  this.dispatchEvent(new CustomEvent("zl-passkey-error", {
2775
2876
  bubbles: true,
@@ -2777,7 +2878,23 @@ let ZlPasskey = class ZlPasskey extends i$2 {
2777
2878
  detail
2778
2879
  }));
2779
2880
  }
2780
- /** Invisible component — no shadow DOM rendering needed. */
2881
+ emitStarted() {
2882
+ const detail = {
2883
+ challenge_id: this.challengeId,
2884
+ method: this.method
2885
+ };
2886
+ this.dispatchEvent(new CustomEvent("zl-passkey-started", {
2887
+ bubbles: true,
2888
+ composed: true,
2889
+ detail
2890
+ }));
2891
+ }
2892
+ /**
2893
+ * Light DOM on purpose: the pending UI must land inside the
2894
+ * orchestrator's shadow root (where the step's adopted styles apply)
2895
+ * without altering the `unsafeHTML` step markup — runtime children don't
2896
+ * participate in its string comparison, so the ceremony never restarts.
2897
+ */
2781
2898
  createRenderRoot() {
2782
2899
  return this;
2783
2900
  }
@@ -2802,6 +2919,10 @@ __decorate([n$1({
2802
2919
  })], ZlPasskey.prototype, "options", null);
2803
2920
  __decorate([n$1({ type: String })], ZlPasskey.prototype, "method", null);
2804
2921
  __decorate([n$1({ type: Boolean })], ZlPasskey.prototype, "manual", null);
2922
+ __decorate([n$1({ attribute: "pending-label" })], ZlPasskey.prototype, "pendingLabel", null);
2923
+ __decorate([n$1({ attribute: "cancel-label" })], ZlPasskey.prototype, "cancelLabel", null);
2924
+ __decorate([n$1({ type: Boolean })], ZlPasskey.prototype, "silent", null);
2925
+ __decorate([r$1()], ZlPasskey.prototype, "pending", null);
2805
2926
  ZlPasskey = __decorate([t$4("zl-passkey")], ZlPasskey);
2806
2927
  const zlPasskeyManifest = {
2807
2928
  tag: "zl-passkey",
@@ -2811,11 +2932,18 @@ const zlPasskeyManifest = {
2811
2932
  "method",
2812
2933
  "challenge-id",
2813
2934
  "options",
2814
- "manual"
2935
+ "manual",
2936
+ "pending-label",
2937
+ "cancel-label",
2938
+ "silent"
2815
2939
  ],
2816
2940
  parts: [],
2817
2941
  slots: [],
2818
- events: ["zl-passkey-result", "zl-passkey-error"]
2942
+ events: [
2943
+ "zl-passkey-result",
2944
+ "zl-passkey-error",
2945
+ "zl-passkey-started"
2946
+ ]
2819
2947
  };
2820
2948
  //#endregion
2821
2949
  //#region ../shared-component-styles/src/lit/pill-host.css?inline
@@ -3365,6 +3493,37 @@ function safeJsonParse(text) {
3365
3493
  return { raw: text };
3366
3494
  }
3367
3495
  }
3496
+ /**
3497
+ * Extracts the server's `{code, message, details}` envelope from an
3498
+ * {@link ApiError} into a display string. Falls back to the fetch layer's
3499
+ * `"METHOD url returned N"` when the body isn't shaped like an envelope, so
3500
+ * transport-level failures (HTML from a proxy, empty 5xx) still say
3501
+ * something.
3502
+ */
3503
+ function apiErrorMessage(error) {
3504
+ const body = error.body;
3505
+ if (!isRecord(body)) return error.message;
3506
+ const serverMessage = typeof body.message === "string" ? body.message : void 0;
3507
+ const detail = pickDetailString(body.details);
3508
+ if (!serverMessage) return error.message;
3509
+ return detail ? `${serverMessage}: ${detail}` : serverMessage;
3510
+ }
3511
+ /**
3512
+ * Reads the innermost human-readable string out of the spec's error
3513
+ * `details` field. Handles both `details: "..."` and the nested
3514
+ * `details: { details: "..." }` shape the platform emits for validation
3515
+ * failures.
3516
+ */
3517
+ function pickDetailString(details) {
3518
+ if (typeof details === "string") return details;
3519
+ if (isRecord(details)) {
3520
+ if (typeof details.details === "string") return details.details;
3521
+ if (typeof details.message === "string") return details.message;
3522
+ }
3523
+ }
3524
+ function isRecord(value) {
3525
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3526
+ }
3368
3527
  //#endregion
3369
3528
  //#region src/orchestrator/api-client.ts
3370
3529
  const apiRequestInit = { credentials: "include" };
@@ -3401,7 +3560,7 @@ async function exchangeSession(api, body, params) {
3401
3560
  * orchestrator's signed-in surfaces to render the user's identity.
3402
3561
  */
3403
3562
  async function getSession$1(api) {
3404
- return await api.getMySession(apiRequestInit);
3563
+ return api.getMySession(apiRequestInit);
3405
3564
  }
3406
3565
  /**
3407
3566
  * Revokes the current session (`DELETE /sessions/me`) with credentials. The
@@ -3505,7 +3664,9 @@ var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
3505
3664
  getMySession: () => getMySession,
3506
3665
  getMyUser: () => getMyUser,
3507
3666
  getOpenIDConfiguration: () => getOpenIDConfiguration,
3667
+ getPatchProjectUrl: () => getPatchProjectUrl,
3508
3668
  getProject: () => getProject,
3669
+ getQueryProjectsUrl: () => getQueryProjectsUrl,
3509
3670
  getReady: () => getReady,
3510
3671
  getRevokeMySessionUrl: () => getRevokeMySessionUrl,
3511
3672
  getRevokeSessionUrl: () => getRevokeSessionUrl,
@@ -3527,6 +3688,8 @@ var zitadelNextGen_exports = /* @__PURE__ */ __exportAll({
3527
3688
  listSchemas: () => listSchemas,
3528
3689
  listSessions: () => listSessions,
3529
3690
  listUsers: () => listUsers,
3691
+ patchProject: () => patchProject,
3692
+ queryProjects: () => queryProjects,
3530
3693
  revokeMySession: () => revokeMySession,
3531
3694
  revokeSession: () => revokeSession,
3532
3695
  revokeToken: () => revokeToken,
@@ -4065,6 +4228,23 @@ const createProject = async (createProjectBody, options) => {
4065
4228
  body: JSON.stringify(createProjectBody)
4066
4229
  });
4067
4230
  };
4231
+ const getQueryProjectsUrl = () => {
4232
+ return `${getProxyPath()}/projects/query`;
4233
+ };
4234
+ /**
4235
+ * @summary Query projects
4236
+ */
4237
+ const queryProjects = async (queryProjectsBody, options) => {
4238
+ return customFetch(getQueryProjectsUrl(), {
4239
+ ...options,
4240
+ method: "POST",
4241
+ headers: {
4242
+ "Content-Type": "application/json",
4243
+ ...options?.headers
4244
+ },
4245
+ body: JSON.stringify(queryProjectsBody)
4246
+ });
4247
+ };
4068
4248
  const getGetProjectUrl = (projectId) => {
4069
4249
  return `${getProxyPath()}/projects/${projectId}`;
4070
4250
  };
@@ -4079,6 +4259,25 @@ const getProject = async (projectId, options) => {
4079
4259
  method: "GET"
4080
4260
  });
4081
4261
  };
4262
+ const getPatchProjectUrl = (projectId) => {
4263
+ return `${getProxyPath()}/projects/${projectId}`;
4264
+ };
4265
+ /**
4266
+ * Updates the state of a project.
4267
+
4268
+ * @summary Update project
4269
+ */
4270
+ const patchProject = async (projectId, patchProjectBody, options) => {
4271
+ return customFetch(getPatchProjectUrl(projectId), {
4272
+ ...options,
4273
+ method: "PATCH",
4274
+ headers: {
4275
+ "Content-Type": "application/json",
4276
+ ...options?.headers
4277
+ },
4278
+ body: JSON.stringify(patchProjectBody)
4279
+ });
4280
+ };
4082
4281
  const getCreateSessionUrl = () => {
4083
4282
  return `${getProxyPath()}/sessions`;
4084
4283
  };
@@ -9506,7 +9705,7 @@ function lookup(locale, key) {
9506
9705
  }
9507
9706
  //#endregion
9508
9707
  //#region src/orchestrator/templates/default.liquid
9509
- var default_default = "<zl-page-shell data-zl-template-root>\n {% if branding.logo_url %}\n <img slot=\"header\" class=\"zl-card-logo\" src=\"{{ branding.logo_url }}\" alt=\"\" />\n {% endif %}\n <zl-card>\n <h1 slot=\"header\" class=\"zl-card-title\">{{ step.texts.title_key | t: identity.display_name }}</h1>\n {% if step.texts.description_key %}\n {% assign card_subtitle = step.texts.description_key | t: identity.display_name %}\n {% if card_subtitle != \"\" %}\n <p slot=\"header\" class=\"zl-card-subtitle\">{{ card_subtitle }}</p>\n {% endif %}\n {% endif %}\n\n {% if errors and errors.size > 0 %}\n {% for err in errors %}\n {% if err | formLevelError %}\n {% assign alert_heading = err.text_key | alertHeading %}\n {% assign alert_body = err.text_key | alertBody %}\n <zl-alert severity=\"error\"{% if alert_heading != \"\" %} heading=\"{{ alert_heading | t }}\"{% endif %}>\n {% if alert_body != \"\" %}{{ alert_body | t }}{% elsif err.text_key %}{{ err.text_key | t }}{% else %}{{ err.message }}{% endif %}\n </zl-alert>\n {% endif %}\n {% endfor %}\n {% endif %}\n\n {% for f in fields %}\n {% assign field_placeholder = f.text_key | fieldPlaceholder %}\n {% assign field_help = f.text_key | fieldHelp %}\n {% assign field_err = f.name | fieldError: errors %}\n {% case f.type %}\n {% when 'checkbox' %}\n <zl-checkbox\n name=\"{{ f.name }}\"\n data-testid=\"zitadel-field-{{ f.name }}\"\n label=\"{{ f.text_key | t }}\"\n value=\"true\"\n {% if f.value %}checked{% endif %}\n {% if f.required %}required{% endif %}\n ></zl-checkbox>\n {% when 'select' %}\n <zl-select\n name=\"{{ f.name }}\"\n data-testid=\"zitadel-field-{{ f.name }}\"\n label=\"{{ f.text_key | t }}\"\n value=\"{{ f.value }}\"\n options='{{ f.validation.enum | selectOptions | json }}'\n {% if f.required %}required{% endif %}\n {% if field_placeholder != \"\" %}placeholder=\"{{ field_placeholder }}\"{% endif %}\n ></zl-select>\n {% else %}\n {% assign autocomplete_val = '' %}\n {% if f.name == 'email' %}\n {% assign autocomplete_val = 'email' %}\n {% elsif f.type == 'password' and step.name contains 'register' %}\n {% assign autocomplete_val = 'new-password' %}\n {% elsif f.type == 'password' %}\n {% assign autocomplete_val = 'current-password' %}\n {% endif %}\n <zl-field\n name=\"{{ f.name }}\"\n data-testid=\"zitadel-field-{{ f.name }}\"\n label=\"{{ f.text_key | t }}\"\n type=\"{{ f.type }}\"\n value=\"{{ f.value }}\"\n {% if autocomplete_val != '' %}autocomplete=\"{{ autocomplete_val }}\"{% endif %}\n {% if f.required %}required{% endif %}\n {% if field_placeholder != \"\" %}placeholder=\"{{ field_placeholder }}\"{% endif %}\n {% if field_err != \"\" %}invalid error=\"{{ field_err }}\"{% endif %}\n >\n {% if field_help != \"\" %}\n <span slot=\"help\">{{ field_help }}</span>\n {% endif %}\n </zl-field>\n {% endcase %}\n {% endfor %}\n\n {% assign recover = actions | where: \"name\", \"recover\" | first %}\n {% if recover %}\n <p class=\"zl-card-forgot\">\n <a href=\"#\" class=\"zl-card-forgot__link\" data-action=\"recover\">{{ recover.text_key | t }}</a>\n </p>\n {% endif %}\n\n {% for a in actions %}\n {% if a.primary %}\n <zl-button\n hierarchy=\"primary\"\n size=\"medium\"\n type=\"submit\"\n block\n action=\"{{ a.name }}\"\n data-testid=\"zitadel-action-{{ a.name }}\"\n label=\"{{ a.text_key | t }}\"\n {% if loading %}loading{% endif %}\n ></zl-button>\n {% endif %}\n {% endfor %}\n\n {% for a in actions %}\n {% unless a.primary or a.name == 'passkey' or a.name == 'register' or a.name == 'sign_in' or a.name == 'recover' %}\n <zl-button\n hierarchy=\"secondary\"\n size=\"medium\"\n type=\"button\"\n block\n action=\"{{ a.name }}\"\n data-testid=\"zitadel-action-{{ a.name }}\"\n label=\"{{ a.text_key | t }}\"\n ></zl-button>\n {% endunless %}\n {% endfor %}\n\n {% assign passkey = actions | where: \"name\", \"passkey\" | first %}\n {% if passkey %}\n {% unless passkey.primary %}\n <zl-button\n hierarchy=\"secondary\"\n size=\"medium\"\n block\n action=\"passkey\"\n data-testid=\"zitadel-action-passkey\"\n label=\"{{ passkey.text_key | t }}\"\n ></zl-button>\n {% endunless %}\n {% endif %}\n\n\n {% assign register = actions | where: \"name\", \"register\" | first %}\n {% if register %}\n <p class=\"zl-card-nav\">\n {{ '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>\n </p>\n {% endif %}\n\n {% assign sign_in = actions | where: \"name\", \"sign_in\" | first %}\n {% if sign_in %}\n <p class=\"zl-card-nav\">\n {{ '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>\n </p>\n {% endif %}\n\n {% if challenge %}\n {% if challenge.method == \"passkey\" or challenge.method == \"passkey_register\" %}\n {% assign ceremony = \"authenticate\" %}\n {% assign submit_method = challenge.method %}\n {% if challenge.method == \"passkey_register\" or challenge.options.user %}\n {% assign ceremony = \"register\" %}\n {% assign submit_method = \"passkey_register\" %}\n {% endif %}\n <zl-passkey\n ceremony=\"{{ ceremony }}\"\n method=\"{{ submit_method }}\"\n challenge-id=\"{{ challenge.challenge_id }}\"\n options='{{ challenge.options | json }}'\n ></zl-passkey>\n {% endif %}\n {% endif %}\n\n {% mandatory_gates %}\n </zl-card>\n</zl-page-shell>\n";
9708
+ var default_default = "<zl-page-shell data-zl-template-root>\n {% if branding.logo_url %}\n <img slot=\"header\" class=\"zl-card-logo\" src=\"{{ branding.logo_url }}\" alt=\"\" />\n {% endif %}\n <zl-card>\n <h1 slot=\"header\" class=\"zl-card-title\">{{ step.texts.title_key | t: identity.display_name }}</h1>\n {% if step.texts.description_key %}\n {% assign card_subtitle = step.texts.description_key | t: identity.display_name %}\n {% if card_subtitle != \"\" %}\n <p slot=\"header\" class=\"zl-card-subtitle\">{{ card_subtitle }}</p>\n {% endif %}\n {% endif %}\n\n {% if errors and errors.size > 0 %}\n {% for err in errors %}\n {% if err | formLevelError %}\n {% assign alert_heading = err.text_key | alertHeading %}\n {% assign alert_body = err.text_key | alertBody %}\n <zl-alert severity=\"error\" dismissible data-zl-step-error{% if alert_heading != \"\" %} heading=\"{{ alert_heading | t }}\"{% endif %}>\n {% if alert_body != \"\" %}{{ alert_body | t }}{% elsif err.text_key %}{{ err.text_key | t }}{% else %}{{ err.message }}{% endif %}\n </zl-alert>\n {% endif %}\n {% endfor %}\n {% endif %}\n\n {% for f in fields %}\n {% assign field_placeholder = f.text_key | fieldPlaceholder %}\n {% assign field_help = f.text_key | fieldHelp %}\n {% assign field_err = f.name | fieldError: errors %}\n {% case f.type %}\n {% when 'checkbox' %}\n <zl-checkbox\n name=\"{{ f.name }}\"\n data-testid=\"zitadel-field-{{ f.name | testid }}\"\n label=\"{{ f.text_key | t }}\"\n value=\"true\"\n {% if f.value %}checked{% endif %}\n {% if f.required %}required{% endif %}\n ></zl-checkbox>\n {% when 'select' %}\n <zl-select\n name=\"{{ f.name }}\"\n data-testid=\"zitadel-field-{{ f.name | testid }}\"\n label=\"{{ f.text_key | t }}\"\n value=\"{{ f.value }}\"\n options='{{ f.validation.enum | selectOptions | json }}'\n {% if f.required %}required{% endif %}\n {% if field_placeholder != \"\" %}placeholder=\"{{ field_placeholder }}\"{% endif %}\n ></zl-select>\n {% else %}\n {% assign autocomplete_val = '' %}\n {% if f.name == 'email' %}\n {% assign autocomplete_val = 'email' %}\n {% elsif f.type == 'password' and step.name contains 'register' %}\n {% assign autocomplete_val = 'new-password' %}\n {% elsif f.type == 'password' %}\n {% assign autocomplete_val = 'current-password' %}\n {% endif %}\n <zl-field\n name=\"{{ f.name }}\"\n data-testid=\"zitadel-field-{{ f.name | testid }}\"\n label=\"{{ f.text_key | t }}\"\n type=\"{{ f.type }}\"\n value=\"{{ f.value }}\"\n {% if autocomplete_val != '' %}autocomplete=\"{{ autocomplete_val }}\"{% endif %}\n {% if f.required %}required{% endif %}\n {% if field_placeholder != \"\" %}placeholder=\"{{ field_placeholder }}\"{% endif %}\n {% if field_err != \"\" %}invalid error=\"{{ field_err }}\"{% endif %}\n >\n {% if field_help != \"\" %}\n <span slot=\"help\">{{ field_help }}</span>\n {% endif %}\n </zl-field>\n {% endcase %}\n {% endfor %}\n\n {% assign recover = actions | where: \"name\", \"recover\" | first %}\n {% if recover %}\n <p class=\"zl-card-forgot\">\n <a href=\"#\" class=\"zl-card-forgot__link\" data-action=\"recover\">{{ recover.text_key | t }}</a>\n </p>\n {% endif %}\n\n {% for a in actions %}\n {% if a.primary %}\n <zl-button\n hierarchy=\"primary\"\n size=\"medium\"\n type=\"submit\"\n block\n action=\"{{ a.name }}\"\n data-testid=\"zitadel-action-{{ a.name }}\"\n label=\"{{ a.text_key | t }}\"\n {% if loading %}loading{% endif %}\n ></zl-button>\n {% endif %}\n {% endfor %}\n\n {% for a in actions %}\n {% unless a.primary or a.name == 'passkey' or a.name == 'register' or a.name == 'sign_in' or a.name == 'recover' %}\n <zl-button\n hierarchy=\"secondary\"\n size=\"medium\"\n type=\"button\"\n block\n action=\"{{ a.name }}\"\n data-testid=\"zitadel-action-{{ a.name }}\"\n label=\"{{ a.text_key | t }}\"\n ></zl-button>\n {% endunless %}\n {% endfor %}\n\n {% assign passkey = actions | where: \"name\", \"passkey\" | first %}\n {% if passkey %}\n {% unless passkey.primary %}\n <zl-button\n hierarchy=\"secondary\"\n size=\"medium\"\n block\n action=\"passkey\"\n data-testid=\"zitadel-action-passkey\"\n label=\"{{ passkey.text_key | t }}\"\n ></zl-button>\n {% endunless %}\n {% endif %}\n\n\n {% assign register = actions | where: \"name\", \"register\" | first %}\n {% if register %}\n <p class=\"zl-card-nav\">\n {{ '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>\n </p>\n {% endif %}\n\n {% assign sign_in = actions | where: \"name\", \"sign_in\" | first %}\n {% if sign_in %}\n <p class=\"zl-card-nav\">\n {{ '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>\n </p>\n {% endif %}\n\n {% if challenge %}\n {% if challenge.method == \"passkey\" or challenge.method == \"passkey_register\" %}\n {% assign ceremony = \"authenticate\" %}\n {% assign submit_method = challenge.method %}\n {% if challenge.method == \"passkey_register\" or challenge.options.user %}\n {% assign ceremony = \"register\" %}\n {% assign submit_method = \"passkey_register\" %}\n {% endif %}\n <zl-passkey\n ceremony=\"{{ ceremony }}\"\n method=\"{{ submit_method }}\"\n challenge-id=\"{{ challenge.challenge_id }}\"\n options='{{ challenge.options | json }}'\n pending-label=\"{{ 'passkey.pending.status' | t }}\"\n cancel-label=\"{{ 'action.cancel' | t }}\"\n ></zl-passkey>\n {% endif %}\n {% endif %}\n\n {% mandatory_gates %}\n </zl-card>\n</zl-page-shell>\n";
9510
9709
  //#endregion
9511
9710
  //#region src/orchestrator/liquid.ts
9512
9711
  /**
@@ -9525,6 +9724,21 @@ var default_default = "<zl-page-shell data-zl-template-root>\n {% if branding.l
9525
9724
  * - Partials are loaded from an in-memory map; no filesystem access.
9526
9725
  */
9527
9726
  const TEMPLATE_NAMES$1 = { default: "default" };
9727
+ /**
9728
+ * Maps `error.*` text keys to a field name (Figma inline-error
9729
+ * annotations). Shared by the template filters (`fieldError` routes these
9730
+ * inline, `formLevelError` suppresses their banner) and by
9731
+ * {@link localiseFlowErrorKeys}, which must downgrade them to a banner
9732
+ * message when the step doesn't render the mapped field.
9733
+ */
9734
+ const fieldErrorKeys = {
9735
+ "error.email_required": "email",
9736
+ "error.email_invalid": "email",
9737
+ "error.email_exists": "email",
9738
+ "error.password_required": "password",
9739
+ "error.password_incorrect": "password",
9740
+ "error.invalid_credentials": "password"
9741
+ };
9528
9742
  function createLiquidEngine(options) {
9529
9743
  const engine = new Liquid({
9530
9744
  templates: {
@@ -9541,7 +9755,7 @@ function createLiquidEngine(options) {
9541
9755
  engine.registerFilter("raw", (value) => stringify(value));
9542
9756
  engine.registerFilter("t", function tFilter(key, ...args) {
9543
9757
  const lookupKey = stringify(key);
9544
- return interpolate(options.locale[lookupKey] ?? lookupKey, args.map(stringify));
9758
+ return interpolate(options.locale[lookupKey] ?? injectedKeyFallback(options.locale, lookupKey) ?? fieldLabelFallback(lookupKey) ?? lookupKey, args.map(stringify));
9545
9759
  });
9546
9760
  /** Resolves `{text_key}.placeholder` — empty when undefined (not the raw key). */
9547
9761
  engine.registerFilter("fieldPlaceholder", (textKey) => {
@@ -9569,15 +9783,6 @@ function createLiquidEngine(options) {
9569
9783
  };
9570
9784
  });
9571
9785
  });
9572
- /** Maps `error.*` text keys to a field name (Figma inline-error annotations). */
9573
- const fieldErrorKeys = {
9574
- "error.email_required": "email",
9575
- "error.email_invalid": "email",
9576
- "error.email_exists": "email",
9577
- "error.password_required": "password",
9578
- "error.password_incorrect": "password",
9579
- "error.invalid_credentials": "password"
9580
- };
9581
9786
  /** Resolves `{text_key}.title` for form-level `<zl-alert heading>`. */
9582
9787
  engine.registerFilter("alertHeading", (textKey) => {
9583
9788
  const lookupKey = `${stringify(textKey)}.title`;
@@ -9598,6 +9803,13 @@ function createLiquidEngine(options) {
9598
9803
  }
9599
9804
  return "";
9600
9805
  });
9806
+ /**
9807
+ * Automation-hook token for a field name: strips the
9808
+ * `x-auth-methods#` credential prefix so testids stay method-named
9809
+ * (`zitadel-field-password`), while the `name` attribute keeps the
9810
+ * raw wire key. See hookName for the contract.
9811
+ */
9812
+ engine.registerFilter("testid", (fieldName) => hookName(stringify(fieldName)));
9601
9813
  /** True when the error should render as `<zl-alert>`, not on a field. */
9602
9814
  engine.registerFilter("formLevelError", (err) => {
9603
9815
  const key = err?.text_key ?? "";
@@ -9611,6 +9823,123 @@ function createLiquidEngine(options) {
9611
9823
  });
9612
9824
  return engine;
9613
9825
  }
9826
+ /**
9827
+ * Fallbacks for text keys the flow engine derives from tenant-chosen step
9828
+ * names (`<step>.action.back` for the injected back action — see
9829
+ * `internal/domain/flow_state_machine.go` `buildStep`). Step names are open,
9830
+ * so no dictionary can enumerate them; a missing step-specific key falls back
9831
+ * to its generic entry instead of leaking the raw key into the UI.
9832
+ */
9833
+ const INJECTED_KEY_FALLBACKS = [{
9834
+ suffix: ".action.back",
9835
+ fallback: "action.back"
9836
+ }];
9837
+ function injectedKeyFallback(locale, key) {
9838
+ for (const { suffix, fallback } of INJECTED_KEY_FALLBACKS) if (key.endsWith(suffix)) return locale[fallback];
9839
+ }
9840
+ /**
9841
+ * Fallback for field-label keys (`<step>.field.<name>` — see
9842
+ * `FlowField.TextKey` in `internal/domain/flow_field_resolver.go`). Both
9843
+ * halves are tenant-chosen (step names and schema property names), so no
9844
+ * catalog can enumerate the keys; a miss renders a humanised property name
9845
+ * ("dateOfBirth" → "Date of birth") instead of leaking the raw key into the
9846
+ * form. Sub-keys like `.placeholder`/`.help` are excluded — they resolve
9847
+ * through their own filters, which stay empty on a miss.
9848
+ */
9849
+ function fieldLabelFallback(key) {
9850
+ const index = key.lastIndexOf(".field.");
9851
+ if (index === -1) return void 0;
9852
+ const field = key.slice(index + 7);
9853
+ if (field === "" || field.includes(".")) return void 0;
9854
+ return capitaliseFirst(humaniseFieldName(field));
9855
+ }
9856
+ /**
9857
+ * Rule-suffix fallbacks for the server's field-validation keys
9858
+ * (`error.<field>_<rule>` — see `FlowFieldValidationError.TextKey` in
9859
+ * `internal/domain/flow_field_resolver.go`). Field names come from the
9860
+ * tenant's user schema, so no catalog can enumerate the specific keys;
9861
+ * a miss resolves to the rule's generic entry, interpolated with the
9862
+ * field's label (`{0}`). The server spells the format rule `_invalid`
9863
+ * (the catalog's existing convention, e.g. `error.email_invalid`), so
9864
+ * that suffix takes the format wording; `_unknown_field` (a submitted
9865
+ * name that is not a step field) takes the catch-all.
9866
+ */
9867
+ const FLOW_ERROR_RULE_FALLBACKS = [
9868
+ {
9869
+ suffix: "_required",
9870
+ generic: "error.field_required"
9871
+ },
9872
+ {
9873
+ suffix: "_min_length",
9874
+ generic: "error.field_min_length"
9875
+ },
9876
+ {
9877
+ suffix: "_max_length",
9878
+ generic: "error.field_max_length"
9879
+ },
9880
+ {
9881
+ suffix: "_format",
9882
+ generic: "error.field_format"
9883
+ },
9884
+ {
9885
+ suffix: "_invalid",
9886
+ generic: "error.field_format"
9887
+ },
9888
+ {
9889
+ suffix: "_unknown_field",
9890
+ generic: "error.field_invalid"
9891
+ }
9892
+ ];
9893
+ const FLOW_ERROR_CATCH_ALL_KEY = "error.field_invalid";
9894
+ /**
9895
+ * Localises a `step.error` payload of `error.*` catalog keys —
9896
+ * field-validation violations and general engine failures (e.g.
9897
+ * `error.invalid_credentials`, `error.passkey_invalid`) alike.
9898
+ * Returns `null` unless EVERY `"; "`-joined segment is an `error.*` key
9899
+ * — the caller keeps other payloads (outcome tokens such as
9900
+ * `user_not_found`) verbatim.
9901
+ *
9902
+ * Keys the locale knows pass through as `text_key` entries: the template
9903
+ * localises them via `| t`, and `fieldErrorKeys` routes the known ones
9904
+ * inline to their field. Unknown keys with a recognised rule suffix are
9905
+ * pre-localised here from their generic {@link FLOW_ERROR_RULE_FALLBACKS}
9906
+ * entry; unknown keys without one pass through as `text_key` (matching
9907
+ * `| t`'s behaviour for non-validation keys such as
9908
+ * `error.sign_in_server`, which localises via `.title`/`.body`).
9909
+ */
9910
+ function localiseFlowErrorKeys(raw, ctx) {
9911
+ const segments = raw.split("; ");
9912
+ if (!segments.every((segment) => segment.startsWith("error."))) return null;
9913
+ return segments.map((key) => localiseFlowErrorKey(key, ctx));
9914
+ }
9915
+ function localiseFlowErrorKey(key, ctx) {
9916
+ const inlineField = fieldErrorKeys[key];
9917
+ const orphanedInline = inlineField !== void 0 && ctx.fields !== void 0 && !ctx.fields.includes(inlineField);
9918
+ if (!orphanedInline && ctx.locale[key] !== void 0) return { text_key: key };
9919
+ for (const { suffix, generic } of FLOW_ERROR_RULE_FALLBACKS) {
9920
+ if (!key.endsWith(suffix)) continue;
9921
+ const field = key.slice(6, key.length - suffix.length);
9922
+ if (field === "") break;
9923
+ return { message: capitaliseFirst(interpolate(ctx.locale[generic] ?? ctx.locale[FLOW_ERROR_CATCH_ALL_KEY] ?? "Please check {0}.", [fieldLabel(ctx, field)])) };
9924
+ }
9925
+ if (orphanedInline && ctx.locale[key] !== void 0) return { message: ctx.locale[key] };
9926
+ return { text_key: key };
9927
+ }
9928
+ /** `<step>.field.<name>` from the locale, else a humanised field name. */
9929
+ function fieldLabel(ctx, field) {
9930
+ return ctx.locale[`${ctx.stepName}.field.${field}`] ?? humaniseFieldName(field);
9931
+ }
9932
+ /**
9933
+ * `x-auth-methods#password` → "password", `givenName` → "given name",
9934
+ * `date_of_birth` → "date of birth".
9935
+ */
9936
+ function humaniseFieldName(field) {
9937
+ return (field.includes("#") ? field.split("#").pop() : field).replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_.-]+/g, " ").trim().toLowerCase();
9938
+ }
9939
+ /** Messages may open with a lowercase field label; sentence-case them. */
9940
+ function capitaliseFirst(text) {
9941
+ return text.length > 0 ? text[0]?.toUpperCase() + text.slice(1) : text;
9942
+ }
9614
9943
  function stringify(value) {
9615
9944
  if (value == null) return "";
9616
9945
  if (typeof value === "string") return value;
@@ -9621,6 +9950,11 @@ function stringify(value) {
9621
9950
  return String(value);
9622
9951
  }
9623
9952
  }
9953
+ /**
9954
+ * Positional `{n}` substitution shared by the `| t` filter and the
9955
+ * pre-localising fallback in {@link localiseFlowErrorKeys}, so both
9956
+ * produce byte-identical output for the same template and args.
9957
+ */
9624
9958
  function interpolate(template, args) {
9625
9959
  if (args.length === 0) return template;
9626
9960
  return template.replace(/\{(\d+)\}/g, (match, index) => {
@@ -9738,6 +10072,10 @@ const en = {
9738
10072
  "signed-in.continue": "Continue",
9739
10073
  "signed-in.logout": "Sign out",
9740
10074
  "passkey-login.title": "Sign in with your passkey",
10075
+ "passkey-first.title": "Sign in",
10076
+ "passkey-first.description": "Use your passkey to continue",
10077
+ "passkey-first.action.passkey": "Continue with passkey",
10078
+ "passkey-first.action.email_fallback": "Use email instead",
9741
10079
  "recover.title": "Check your email",
9742
10080
  "recover.description": "We sent a password reset link to your email address.",
9743
10081
  "recover.action.back": "Back to sign in",
@@ -9745,12 +10083,17 @@ const en = {
9745
10083
  "submit.signin": "Sign in",
9746
10084
  "action.forgot_password": "Forgot password?",
9747
10085
  "action.cancel": "Cancel",
10086
+ "action.back": "Back",
9748
10087
  "sso.redirect.title": "Redirecting to your provider…",
9749
- "error.passkey_cancelled": "Passkey setup was cancelled",
10088
+ "passkey.pending.status": "Waiting for your passkey…",
10089
+ "error.passkey_cancelled": "The passkey prompt was closed before completing.",
10090
+ "error.passkey_timeout": "The passkey request timed out. Please try again.",
9750
10091
  "error.passkey_not_registered": "This passkey is not registered. Please sign in with your email and password.",
9751
10092
  "error.passkey_setup_failed": "Passkey registration did not complete. Please try again.",
9752
10093
  "error.passkey_unsupported": "This device does not support passkeys",
9753
10094
  "error.passkey_failed": "Something went wrong. Please try again.",
10095
+ "error.passkey_invalid": "This passkey could not be verified. Please try again.",
10096
+ "error.passkey_registration_invalid": "The new passkey could not be verified. Please try registering it again.",
9754
10097
  "error.email_required": "Please enter an email address",
9755
10098
  "error.email_invalid": "Please enter a valid email",
9756
10099
  "error.password_required": "Please enter a password",
@@ -9759,6 +10102,11 @@ const en = {
9759
10102
  /** Figma sign-in error `6602:180268` — inline on password field. */
9760
10103
  "error.invalid_credentials": "Wrong email or password.",
9761
10104
  "error.required": "This field is required.",
10105
+ "error.field_required": "{0} is required.",
10106
+ "error.field_format": "Please enter a valid {0}.",
10107
+ "error.field_min_length": "{0} is too short.",
10108
+ "error.field_max_length": "{0} is too long.",
10109
+ "error.field_invalid": "Please check {0}.",
9762
10110
  "error.sign_in_server.title": "We couldn't complete your sign in.",
9763
10111
  "error.sign_in_server.body": "Please try again in a few minutes"
9764
10112
  };
@@ -9840,6 +10188,10 @@ const de = {
9840
10188
  "signed-in.continue": "Weiter",
9841
10189
  "signed-in.logout": "Abmelden",
9842
10190
  "passkey-login.title": "Mit Passkey anmelden",
10191
+ "passkey-first.title": "Anmelden",
10192
+ "passkey-first.description": "Verwende deinen Passkey, um fortzufahren",
10193
+ "passkey-first.action.passkey": "Weiter mit Passkey",
10194
+ "passkey-first.action.email_fallback": "Stattdessen E-Mail verwenden",
9843
10195
  "recover.title": "E-Mail prüfen",
9844
10196
  "recover.description": "Wir haben einen Link zum Zurücksetzen des Passworts an deine E-Mail-Adresse gesendet.",
9845
10197
  "recover.action.back": "Zurück zur Anmeldung",
@@ -9847,12 +10199,17 @@ const de = {
9847
10199
  "submit.signin": "Anmelden",
9848
10200
  "action.forgot_password": "Passwort vergessen?",
9849
10201
  "action.cancel": "Abbrechen",
10202
+ "action.back": "Zurück",
9850
10203
  "sso.redirect.title": "Weiterleitung zum Anbieter…",
9851
- "error.passkey_cancelled": "Passkey-Einrichtung wurde abgebrochen",
10204
+ "passkey.pending.status": "Warten auf deinen Passkey…",
10205
+ "error.passkey_cancelled": "Die Passkey-Abfrage wurde vorzeitig geschlossen.",
10206
+ "error.passkey_timeout": "Die Passkey-Anfrage hat zu lange gedauert. Bitte versuche es erneut.",
9852
10207
  "error.passkey_not_registered": "Dieser Passkey ist nicht registriert. Bitte melde dich mit E-Mail und Passwort an.",
9853
10208
  "error.passkey_setup_failed": "Passkey-Registrierung wurde nicht abgeschlossen. Bitte versuche es erneut.",
9854
10209
  "error.passkey_unsupported": "Dieses Gerät unterstützt keine Passkeys",
9855
10210
  "error.passkey_failed": "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
10211
+ "error.passkey_invalid": "Dieser Passkey konnte nicht bestätigt werden. Bitte versuche es erneut.",
10212
+ "error.passkey_registration_invalid": "Der neue Passkey konnte nicht bestätigt werden. Bitte registriere ihn erneut.",
9856
10213
  "error.email_required": "Bitte gib eine E-Mail-Adresse ein",
9857
10214
  "error.email_invalid": "Bitte gib eine gültige E-Mail-Adresse ein",
9858
10215
  "error.password_required": "Bitte gib ein Passwort ein",
@@ -9860,6 +10217,11 @@ const de = {
9860
10217
  "error.email_exists": "Ein Konto mit dieser E-Mail-Adresse existiert bereits.",
9861
10218
  "error.invalid_credentials": "Falsche E-Mail oder falsches Passwort.",
9862
10219
  "error.required": "Dieses Feld ist erforderlich.",
10220
+ "error.field_required": "{0} ist erforderlich.",
10221
+ "error.field_format": "Bitte gib einen gültigen Wert für {0} ein.",
10222
+ "error.field_min_length": "{0} ist zu kurz.",
10223
+ "error.field_max_length": "{0} ist zu lang.",
10224
+ "error.field_invalid": "Bitte überprüfe {0}.",
9863
10225
  "error.sign_in_server.title": "Anmeldung konnte nicht abgeschlossen werden.",
9864
10226
  "error.sign_in_server.body": "Bitte versuche es in einigen Minuten erneut"
9865
10227
  };
@@ -9944,6 +10306,10 @@ const builtinLocales = {
9944
10306
  "signed-in.continue": "Continua",
9945
10307
  "signed-in.logout": "Esci",
9946
10308
  "passkey-login.title": "Accedi con la tua passkey",
10309
+ "passkey-first.title": "Accedi",
10310
+ "passkey-first.description": "Usa la tua passkey per continuare",
10311
+ "passkey-first.action.passkey": "Continua con passkey",
10312
+ "passkey-first.action.email_fallback": "Usa invece l'email",
9947
10313
  "recover.title": "Controlla la tua e-mail",
9948
10314
  "recover.description": "Abbiamo inviato un link per reimpostare la password al tuo indirizzo e-mail.",
9949
10315
  "recover.action.back": "Torna all'accesso",
@@ -9951,12 +10317,17 @@ const builtinLocales = {
9951
10317
  "submit.signin": "Accedi",
9952
10318
  "action.forgot_password": "Password dimenticata?",
9953
10319
  "action.cancel": "Annulla",
10320
+ "action.back": "Indietro",
9954
10321
  "sso.redirect.title": "Reindirizzamento al provider…",
9955
- "error.passkey_cancelled": "La configurazione della passkey è stata annullata",
10322
+ "passkey.pending.status": "In attesa della tua passkey",
10323
+ "error.passkey_cancelled": "La richiesta della passkey è stata chiusa prima del completamento.",
10324
+ "error.passkey_timeout": "La richiesta della passkey è scaduta. Riprova.",
9956
10325
  "error.passkey_not_registered": "Questa passkey non è registrata. Accedi con e-mail e password.",
9957
10326
  "error.passkey_setup_failed": "La registrazione della passkey non è stata completata. Riprova.",
9958
10327
  "error.passkey_unsupported": "Questo dispositivo non supporta le passkey",
9959
10328
  "error.passkey_failed": "Qualcosa è andato storto. Riprova.",
10329
+ "error.passkey_invalid": "Non è stato possibile verificare questa passkey. Riprova.",
10330
+ "error.passkey_registration_invalid": "Non è stato possibile verificare la nuova passkey. Riprova a registrarla.",
9960
10331
  "error.email_required": "Inserisci un indirizzo e-mail",
9961
10332
  "error.email_invalid": "Inserisci un indirizzo e-mail valido",
9962
10333
  "error.password_required": "Inserisci una password",
@@ -9964,6 +10335,11 @@ const builtinLocales = {
9964
10335
  "error.email_exists": "Esiste già un account con questo indirizzo e-mail.",
9965
10336
  "error.invalid_credentials": "E-mail o password errata.",
9966
10337
  "error.required": "Questo campo è obbligatorio.",
10338
+ "error.field_required": "{0} è obbligatorio.",
10339
+ "error.field_format": "Inserisci un valore valido per {0}.",
10340
+ "error.field_min_length": "{0} è troppo corto.",
10341
+ "error.field_max_length": "{0} è troppo lungo.",
10342
+ "error.field_invalid": "Controlla {0}.",
9967
10343
  "error.sign_in_server.title": "Non è stato possibile completare l'accesso.",
9968
10344
  "error.sign_in_server.body": "Riprova tra qualche minuto"
9969
10345
  }
@@ -11897,6 +12273,13 @@ let ZitadelLogin = class ZitadelLogin extends i$2 {
11897
12273
  set purpose(value) {
11898
12274
  this.#_purpose_accessor_storage = value;
11899
12275
  }
12276
+ #_flowName_accessor_storage = "";
12277
+ get flowName() {
12278
+ return this.#_flowName_accessor_storage;
12279
+ }
12280
+ set flowName(value) {
12281
+ this.#_flowName_accessor_storage = value;
12282
+ }
11900
12283
  #_project_accessor_storage;
11901
12284
  get project() {
11902
12285
  return this.#_project_accessor_storage;
@@ -12004,6 +12387,8 @@ let ZitadelLogin = class ZitadelLogin extends i$2 {
12004
12387
  sheet.replaceSync(layout_chrome_default);
12005
12388
  root.adoptedStyleSheets = [...existing, sheet];
12006
12389
  root.addEventListener("zl-input", this.handleAtomInput);
12390
+ root.addEventListener("zl-change", this.handleAtomEdited);
12391
+ root.addEventListener("zl-dismiss", this.handleAlertDismiss);
12007
12392
  root.addEventListener("zl-submit", this.handleAtomSubmit);
12008
12393
  root.addEventListener("click", this.handleDelegatedAction);
12009
12394
  root.addEventListener("submit", this.handleFormSubmit);
@@ -12142,17 +12527,32 @@ let ZitadelLogin = class ZitadelLogin extends i$2 {
12142
12527
  if (!cfg.projectId) throw new Error("<zitadel-login> requires a project id (the `project-id` attribute, `configureZitadel({ projectId })`, or a `project` handle) to start a flow.");
12143
12528
  wire = await startFlow(api, {
12144
12529
  project_id: cfg.projectId,
12145
- purpose: this.purpose
12530
+ purpose: this.purpose,
12531
+ ...this.flowName ? { flow_definition_name: this.flowName } : {}
12146
12532
  });
12147
12533
  }
12148
12534
  this.applyResponse(wire);
12149
12535
  } catch (error) {
12150
- this.handleTransportError(error);
12536
+ this.handleTransportError(this.describeFlowSelectionError(error));
12151
12537
  } finally {
12152
12538
  this.loading = false;
12153
12539
  }
12154
12540
  }
12541
+ /**
12542
+ * When a `flow-name` lookup fails, the server's envelope only says
12543
+ * "not found" / "purpose mismatch" — it cannot know the name came from
12544
+ * an attribute. Rewrap those two codes with the attribute and the fix;
12545
+ * every other error passes through untouched.
12546
+ */
12547
+ describeFlowSelectionError(error) {
12548
+ if (!this.flowName || !(error instanceof ApiError)) return error;
12549
+ const code = typeof error.body === "object" && error.body !== null && "code" in error.body ? String(error.body.code) : "";
12550
+ if (code === "flowdef.not_found") return /* @__PURE__ */ new Error(`<zitadel-login> flow-name="${this.flowName}" does not match any active flow definition in this project. Check the \`name\` in your flow file and that it has been applied (\`zitadel apply\`).`);
12551
+ if (code === "flowdef.purpose_mismatch") return /* @__PURE__ */ new Error(`<zitadel-login> flow-name="${this.flowName}" matched a flow definition that does not serve purpose "${this.purpose}".`);
12552
+ return error;
12553
+ }
12155
12554
  applyResponse(wire) {
12555
+ this.stepErrorDismissed = false;
12156
12556
  this.response = wire;
12157
12557
  const { branding, issues } = validateBranding(wire.branding);
12158
12558
  this.branding = branding;
@@ -12206,7 +12606,12 @@ let ZitadelLogin = class ZitadelLogin extends i$2 {
12206
12606
  }
12207
12607
  renderStep(step, engine) {
12208
12608
  const tenantSource = typeof this.branding?.liquid_template === "string" && this.branding.liquid_template.length > 0 ? this.branding.liquid_template : null;
12209
- const errors = step.error ? step.error.startsWith("error.") ? [{ text_key: step.error }] : [{ message: step.error }] : [];
12609
+ const rawErrors = step.error ? localiseFlowErrorKeys(step.error, {
12610
+ locale: this.resolveLocale(),
12611
+ stepName: step.name ?? "",
12612
+ fields: (step.fields ?? []).map((field) => field.name)
12613
+ }) ?? [{ message: step.error }] : [];
12614
+ const errors = this.loading && this.stepErrorDismissed ? [] : rawErrors;
12210
12615
  const fields = step.fields ?? [];
12211
12616
  const actions = step.actions ?? [];
12212
12617
  const context = {
@@ -12300,6 +12705,17 @@ let ZitadelLogin = class ZitadelLogin extends i$2 {
12300
12705
  if (native && native.value !== field.value) return native.value;
12301
12706
  return typeof field.value === "string" ? field.value : void 0;
12302
12707
  }
12708
+ /**
12709
+ * True once the user retired the current step error by editing a field or
12710
+ * dismissing the alert. Deliberately NON-reactive: consulting reactive
12711
+ * state in `renderStep` would change its output string on the first
12712
+ * post-dismiss keystroke, and `unsafeHTML` would rebuild the whole step
12713
+ * subtree — wiping typed values (`hydrateStepAfterRender` only re-applies
12714
+ * them on response changes) and reconnecting atoms. Reset on every new
12715
+ * response; consulted only by the `loading` re-render, which rebuilds
12716
+ * anyway.
12717
+ */
12718
+ stepErrorDismissed = false;
12303
12719
  handleAtomInput = (event) => {
12304
12720
  if (!event.detail) return;
12305
12721
  const { name, value } = event.detail;
@@ -12309,11 +12725,41 @@ let ZitadelLogin = class ZitadelLogin extends i$2 {
12309
12725
  [name]: value
12310
12726
  };
12311
12727
  this.syncFieldElementValue(name, value);
12728
+ this.clearStaleErrors(name);
12312
12729
  emit(this, "zitadel-flow-input", {
12313
12730
  name,
12314
12731
  value
12315
12732
  });
12316
12733
  };
12734
+ /** `zl-change` from <zl-checkbox>/<zl-select>: only error clearing. */
12735
+ handleAtomEdited = (event) => {
12736
+ const name = event.detail?.name;
12737
+ if (name) this.clearStaleErrors(name);
12738
+ };
12739
+ /** Explicit dismiss of the step-error alert (it removes itself). */
12740
+ handleAlertDismiss = (event) => {
12741
+ if (event.target?.matches?.("zl-alert[data-zl-step-error]")) this.stepErrorDismissed = true;
12742
+ };
12743
+ /**
12744
+ * Retire the current step error after the user edits `fieldName`:
12745
+ * remove the form-level alert(s) and clear the edited field's inline
12746
+ * error — other fields' inline errors stay until they are edited.
12747
+ * Imperative on purpose; see {@link stepErrorDismissed}.
12748
+ */
12749
+ clearStaleErrors(fieldName) {
12750
+ if (!this.response?.step.error) return;
12751
+ const root = this.shadowRoot;
12752
+ if (!root) return;
12753
+ if (!this.stepErrorDismissed) {
12754
+ this.stepErrorDismissed = true;
12755
+ for (const alert of root.querySelectorAll("zl-alert[data-zl-step-error]")) alert.remove();
12756
+ }
12757
+ for (const field of root.querySelectorAll("zl-field")) {
12758
+ if (field.getAttribute("name") !== fieldName || !field.invalid) continue;
12759
+ field.invalid = false;
12760
+ field.error = "";
12761
+ }
12762
+ }
12317
12763
  syncFieldElementValue(name, value) {
12318
12764
  const root = this.shadowRoot;
12319
12765
  if (!root) return;
@@ -12373,9 +12819,10 @@ let ZitadelLogin = class ZitadelLogin extends i$2 {
12373
12819
  */
12374
12820
  handlePasskeyError = (event) => {
12375
12821
  if (!this.response) return;
12376
- const { error: message, aborted } = event.detail;
12377
- const errorKey = aborted ? "error.passkey_cancelled" : "error.passkey_failed";
12822
+ const { error: message, aborted, timed_out: timedOut } = event.detail;
12823
+ const errorKey = timedOut ? "error.passkey_timeout" : aborted ? "error.passkey_cancelled" : "error.passkey_failed";
12378
12824
  if (this.response.step.error === errorKey) return;
12825
+ this.stepErrorDismissed = false;
12379
12826
  const { challenge: _dropped, ...stepWithoutChallenge } = this.response.step;
12380
12827
  this.response = {
12381
12828
  ...this.response,
@@ -12384,7 +12831,7 @@ let ZitadelLogin = class ZitadelLogin extends i$2 {
12384
12831
  error: errorKey
12385
12832
  }
12386
12833
  };
12387
- console.warn(`[zitadel-login] passkey ceremony ${aborted ? "cancelled" : "failed"}: ${message}`);
12834
+ console.warn(`[zitadel-login] passkey ceremony ${timedOut ? "timed out" : aborted ? "cancelled" : "failed"}: ${message}`);
12388
12835
  };
12389
12836
  findPrimaryAction() {
12390
12837
  const root = this.shadowRoot;
@@ -12426,13 +12873,17 @@ let ZitadelLogin = class ZitadelLogin extends i$2 {
12426
12873
  }
12427
12874
  }
12428
12875
  handleTransportError(error) {
12429
- const message = error instanceof Error ? error.message : "Unexpected error contacting the Flow API.";
12876
+ const message = error instanceof ApiError ? apiErrorMessage(error) : error instanceof Error ? error.message : "Unexpected error contacting the Flow API.";
12430
12877
  this.startupError = message;
12431
12878
  console.error("[zitadel-login]", error);
12432
12879
  emit(this, "zitadel-flow-error", { message });
12433
12880
  }
12434
12881
  };
12435
12882
  __decorate([n$1({ type: String })], ZitadelLogin.prototype, "purpose", null);
12883
+ __decorate([n$1({
12884
+ type: String,
12885
+ attribute: "flow-name"
12886
+ })], ZitadelLogin.prototype, "flowName", null);
12436
12887
  __decorate([n$1({ attribute: false })], ZitadelLogin.prototype, "project", null);
12437
12888
  __decorate([n$1({
12438
12889
  type: String,