@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
@@ -0,0 +1,145 @@
1
+ import { Liquid } from "liquidjs";
2
+ //#region src/template.ts
3
+ /**
4
+ * The authoritative login-template validator (ADR 040).
5
+ *
6
+ * Runs where authoring happens — `zitadel plan`/`apply`, and later editors —
7
+ * against the same LiquidJS dialect `@zitadel/components` renders. The Go
8
+ * server cannot parse this dialect faithfully, so it only mirrors the
9
+ * lexical subset below (`internal/domain/branding_validator.go`, see the
10
+ * `goRef` entries in {@link TEMPLATE_VALIDATION_RULES}); keep the two
11
+ * pattern lists in sync.
12
+ *
13
+ * Render-time safety does not depend on this validator: the component
14
+ * escapes all output, neuters `| raw`, sanitises via DOMPurify, and patches
15
+ * missing required UI with `{% mandatory_gates %}`
16
+ * (`docs/design/flowengine/template-security.md`).
17
+ */
18
+ /** The `{% mandatory_gates %}` runtime safety-net tag every template must emit. */
19
+ const MANDATORY_GATES_TAG = "mandatory_gates";
20
+ /**
21
+ * Custom Liquid tags the login orchestrator registers beyond the standard
22
+ * set. Validators must register them (as no-ops) or parsing valid templates
23
+ * fails; `@zitadel/components` registers the real implementations.
24
+ */
25
+ const TEMPLATE_TAGS = [MANDATORY_GATES_TAG];
26
+ /**
27
+ * Byte cap for stored templates. Mirrors `maxLength` on
28
+ * `branding.liquid_template` in the OpenAPI contract and
29
+ * `MaxBrandingTemplateBytes` in the Go gate.
30
+ */
31
+ const MAX_TEMPLATE_BYTES = 131072;
32
+ /**
33
+ * Rule catalog with pointers into the Go lexical gate
34
+ * (`internal/domain/branding_validator.go`) where a server-side mirror
35
+ * exists. `parse` and `mandatory-gates` are authoring-only: the server
36
+ * cannot check them (wrong dialect) and render-time behavior degrades
37
+ * safely without them.
38
+ */
39
+ const TEMPLATE_VALIDATION_RULES = {
40
+ parse: { goRef: null },
41
+ size: { goRef: "MaxBrandingTemplateBytes" },
42
+ "no-script-tag": { goRef: "brandingScriptTag" },
43
+ "no-style-tag": { goRef: "brandingStyleTag" },
44
+ "no-inline-handlers": { goRef: "brandingInlineHandler" },
45
+ "no-javascript-urls": { goRef: "brandingJavascriptURL" },
46
+ "no-raw-filter": { goRef: "brandingRawFilter" },
47
+ "mandatory-gates": { goRef: null }
48
+ };
49
+ /**
50
+ * Lexical bans, identical to the Go gate. The inline-handler pattern only
51
+ * matches inside an open tag so Liquid like `{% assign online = true %}`
52
+ * doesn't false-positive; its separator class includes `/` because HTML
53
+ * treats a slash between attributes as whitespace (`<img/onerror=…>`).
54
+ */
55
+ const BANNED_TEMPLATE_PATTERNS = [
56
+ {
57
+ rule: "no-script-tag",
58
+ pattern: /<\s*script\b/i,
59
+ message: "Templates must not contain <script> tags."
60
+ },
61
+ {
62
+ rule: "no-style-tag",
63
+ pattern: /<\s*style\b/i,
64
+ message: "Templates must not contain <style> tags — theming is orchestrator-owned via tokens."
65
+ },
66
+ {
67
+ rule: "no-inline-handlers",
68
+ pattern: /<[a-z][^>]*[\s/]on[a-z]+\s*=/i,
69
+ message: "Templates must not contain inline event handlers (on*= attributes)."
70
+ },
71
+ {
72
+ rule: "no-javascript-urls",
73
+ pattern: /javascript\s*:/i,
74
+ message: "Templates must not contain javascript: URLs."
75
+ },
76
+ {
77
+ rule: "no-raw-filter",
78
+ pattern: /\|\s*raw\b/,
79
+ message: "Templates must not use the | raw filter. The renderer neuters it anyway; remove it from the template."
80
+ }
81
+ ];
82
+ const MANDATORY_GATES_PATTERN = new RegExp(`\\{%-?\\s*${MANDATORY_GATES_TAG}\\s*-?%\\}`);
83
+ let parseEngine;
84
+ /**
85
+ * A parse-only engine mirroring the orchestrator's dialect options
86
+ * (`packages/components/src/orchestrator/liquid.ts`); custom tags are
87
+ * registered as no-ops because only parseability matters here.
88
+ */
89
+ function getParseEngine() {
90
+ if (!parseEngine) {
91
+ const engine = new Liquid({
92
+ cache: false,
93
+ jsTruthy: true,
94
+ relativeReference: false,
95
+ strictFilters: false,
96
+ strictVariables: false
97
+ });
98
+ for (const tag of TEMPLATE_TAGS) engine.registerTag(tag, {
99
+ parse() {},
100
+ render() {
101
+ return "";
102
+ }
103
+ });
104
+ parseEngine = engine;
105
+ }
106
+ return parseEngine;
107
+ }
108
+ /**
109
+ * Validates a login template. Pure and non-throwing, like
110
+ * `validateFlowDefinition`: returns an issue list, empty when the template
111
+ * is valid.
112
+ */
113
+ function validateLoginTemplate(template) {
114
+ const issues = [];
115
+ const bytes = new TextEncoder().encode(template).length;
116
+ if (bytes > 131072) issues.push({
117
+ severity: "error",
118
+ rule: "size",
119
+ message: `Template is ${bytes} bytes; the platform caps liquid_template at ${MAX_TEMPLATE_BYTES} bytes.`
120
+ });
121
+ for (const banned of BANNED_TEMPLATE_PATTERNS) if (banned.pattern.test(template)) issues.push({
122
+ severity: "error",
123
+ rule: banned.rule,
124
+ message: banned.message
125
+ });
126
+ if (!MANDATORY_GATES_PATTERN.test(template)) issues.push({
127
+ severity: "error",
128
+ rule: "mandatory-gates",
129
+ message: "Template must contain a trailing {% mandatory_gates %} tag — the runtime safety net that appends required fields, gates, and the submit action a template forgot to render."
130
+ });
131
+ try {
132
+ getParseEngine().parse(template);
133
+ } catch (error) {
134
+ issues.push({
135
+ severity: "error",
136
+ rule: "parse",
137
+ message: `Template is not valid LiquidJS: ${error instanceof Error ? error.message : String(error)}`
138
+ });
139
+ }
140
+ return issues;
141
+ }
142
+ //#endregion
143
+ export { BANNED_TEMPLATE_PATTERNS, MANDATORY_GATES_TAG, MAX_TEMPLATE_BYTES, TEMPLATE_TAGS, TEMPLATE_VALIDATION_RULES, validateLoginTemplate };
144
+
145
+ //# sourceMappingURL=template.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.mjs","names":[],"sources":["../src/template.ts"],"sourcesContent":["/**\n * The authoritative login-template validator (ADR 040).\n *\n * Runs where authoring happens — `zitadel plan`/`apply`, and later editors —\n * against the same LiquidJS dialect `@zitadel/components` renders. The Go\n * server cannot parse this dialect faithfully, so it only mirrors the\n * lexical subset below (`internal/domain/branding_validator.go`, see the\n * `goRef` entries in {@link TEMPLATE_VALIDATION_RULES}); keep the two\n * pattern lists in sync.\n *\n * Render-time safety does not depend on this validator: the component\n * escapes all output, neuters `| raw`, sanitises via DOMPurify, and patches\n * missing required UI with `{% mandatory_gates %}`\n * (`docs/design/flowengine/template-security.md`).\n */\nimport { Liquid } from \"liquidjs\";\n\n/** The `{% mandatory_gates %}` runtime safety-net tag every template must emit. */\nexport const MANDATORY_GATES_TAG = \"mandatory_gates\";\n\n/**\n * Custom Liquid tags the login orchestrator registers beyond the standard\n * set. Validators must register them (as no-ops) or parsing valid templates\n * fails; `@zitadel/components` registers the real implementations.\n */\nexport const TEMPLATE_TAGS = [MANDATORY_GATES_TAG] as const;\n\n/**\n * Byte cap for stored templates. Mirrors `maxLength` on\n * `branding.liquid_template` in the OpenAPI contract and\n * `MaxBrandingTemplateBytes` in the Go gate.\n */\nexport const MAX_TEMPLATE_BYTES = 131072;\n\nexport type TemplateValidationSeverity = \"error\" | \"warning\";\n\n/**\n * Rule catalog with pointers into the Go lexical gate\n * (`internal/domain/branding_validator.go`) where a server-side mirror\n * exists. `parse` and `mandatory-gates` are authoring-only: the server\n * cannot check them (wrong dialect) and render-time behavior degrades\n * safely without them.\n */\nexport const TEMPLATE_VALIDATION_RULES = {\n parse: { goRef: null },\n size: { goRef: \"MaxBrandingTemplateBytes\" },\n \"no-script-tag\": { goRef: \"brandingScriptTag\" },\n \"no-style-tag\": { goRef: \"brandingStyleTag\" },\n \"no-inline-handlers\": { goRef: \"brandingInlineHandler\" },\n \"no-javascript-urls\": { goRef: \"brandingJavascriptURL\" },\n \"no-raw-filter\": { goRef: \"brandingRawFilter\" },\n \"mandatory-gates\": { goRef: null },\n} as const;\n\nexport type TemplateValidationRuleId = keyof typeof TEMPLATE_VALIDATION_RULES;\n\nexport type TemplateValidationIssue = {\n severity: TemplateValidationSeverity;\n rule: TemplateValidationRuleId;\n message: string;\n};\n\ntype BannedPattern = {\n rule: TemplateValidationRuleId;\n pattern: RegExp;\n message: string;\n};\n\n/**\n * Lexical bans, identical to the Go gate. The inline-handler pattern only\n * matches inside an open tag so Liquid like `{% assign online = true %}`\n * doesn't false-positive; its separator class includes `/` because HTML\n * treats a slash between attributes as whitespace (`<img/onerror=…>`).\n */\nexport const BANNED_TEMPLATE_PATTERNS: readonly BannedPattern[] = [\n {\n rule: \"no-script-tag\",\n pattern: /<\\s*script\\b/i,\n message: \"Templates must not contain <script> tags.\",\n },\n {\n rule: \"no-style-tag\",\n pattern: /<\\s*style\\b/i,\n message: \"Templates must not contain <style> tags — theming is orchestrator-owned via tokens.\",\n },\n {\n rule: \"no-inline-handlers\",\n pattern: /<[a-z][^>]*[\\s/]on[a-z]+\\s*=/i,\n message: \"Templates must not contain inline event handlers (on*= attributes).\",\n },\n {\n rule: \"no-javascript-urls\",\n pattern: /javascript\\s*:/i,\n message: \"Templates must not contain javascript: URLs.\",\n },\n {\n rule: \"no-raw-filter\",\n pattern: /\\|\\s*raw\\b/,\n message:\n \"Templates must not use the | raw filter. The renderer neuters it anyway; remove it from the template.\",\n },\n] as const;\n\n// Derived from the exported tag name so a rename cannot silently diverge\n// the validator from the renderer contract.\nconst MANDATORY_GATES_PATTERN = new RegExp(`\\\\{%-?\\\\s*${MANDATORY_GATES_TAG}\\\\s*-?%\\\\}`);\n\nlet parseEngine: Liquid | undefined;\n\n/**\n * A parse-only engine mirroring the orchestrator's dialect options\n * (`packages/components/src/orchestrator/liquid.ts`); custom tags are\n * registered as no-ops because only parseability matters here.\n */\nfunction getParseEngine(): Liquid {\n if (!parseEngine) {\n const engine = new Liquid({\n cache: false,\n jsTruthy: true,\n relativeReference: false,\n strictFilters: false,\n strictVariables: false,\n });\n for (const tag of TEMPLATE_TAGS) {\n engine.registerTag(tag, {\n parse() {\n // no-op: the tag takes no arguments and no body\n },\n render() {\n return \"\";\n },\n });\n }\n parseEngine = engine;\n }\n return parseEngine;\n}\n\n/**\n * Validates a login template. Pure and non-throwing, like\n * `validateFlowDefinition`: returns an issue list, empty when the template\n * is valid.\n */\nexport function validateLoginTemplate(template: string): TemplateValidationIssue[] {\n const issues: TemplateValidationIssue[] = [];\n\n const bytes = new TextEncoder().encode(template).length;\n if (bytes > MAX_TEMPLATE_BYTES) {\n issues.push({\n severity: \"error\",\n rule: \"size\",\n message: `Template is ${bytes} bytes; the platform caps liquid_template at ${MAX_TEMPLATE_BYTES} bytes.`,\n });\n }\n\n for (const banned of BANNED_TEMPLATE_PATTERNS) {\n if (banned.pattern.test(template)) {\n issues.push({ severity: \"error\", rule: banned.rule, message: banned.message });\n }\n }\n\n if (!MANDATORY_GATES_PATTERN.test(template)) {\n issues.push({\n severity: \"error\",\n rule: \"mandatory-gates\",\n message:\n \"Template must contain a trailing {% mandatory_gates %} tag — the runtime safety net that appends required fields, gates, and the submit action a template forgot to render.\",\n });\n }\n\n try {\n getParseEngine().parse(template);\n } catch (error) {\n issues.push({\n severity: \"error\",\n rule: \"parse\",\n message: `Template is not valid LiquidJS: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n\n return issues;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkBA,MAAa,sBAAsB;;;;;;AAOnC,MAAa,gBAAgB,CAAC,oBAAoB;;;;;;AAOlD,MAAa,qBAAqB;;;;;;;;AAWlC,MAAa,4BAA4B;CACvC,OAAO,EAAE,OAAO,MAAM;CACtB,MAAM,EAAE,OAAO,4BAA4B;CAC3C,iBAAiB,EAAE,OAAO,qBAAqB;CAC/C,gBAAgB,EAAE,OAAO,oBAAoB;CAC7C,sBAAsB,EAAE,OAAO,yBAAyB;CACxD,sBAAsB,EAAE,OAAO,yBAAyB;CACxD,iBAAiB,EAAE,OAAO,qBAAqB;CAC/C,mBAAmB,EAAE,OAAO,MAAM;CACnC;;;;;;;AAsBD,MAAa,2BAAqD;CAChE;EACE,MAAM;EACN,SAAS;EACT,SAAS;EACV;CACD;EACE,MAAM;EACN,SAAS;EACT,SAAS;EACV;CACD;EACE,MAAM;EACN,SAAS;EACT,SAAS;EACV;CACD;EACE,MAAM;EACN,SAAS;EACT,SAAS;EACV;CACD;EACE,MAAM;EACN,SAAS;EACT,SACE;EACH;CACF;AAID,MAAM,0BAA0B,IAAI,OAAO,aAAa,oBAAoB,YAAY;AAExF,IAAI;;;;;;AAOJ,SAAS,iBAAyB;AAChC,KAAI,CAAC,aAAa;EAChB,MAAM,SAAS,IAAI,OAAO;GACxB,OAAO;GACP,UAAU;GACV,mBAAmB;GACnB,eAAe;GACf,iBAAiB;GAClB,CAAC;AACF,OAAK,MAAM,OAAO,cAChB,QAAO,YAAY,KAAK;GACtB,QAAQ;GAGR,SAAS;AACP,WAAO;;GAEV,CAAC;AAEJ,gBAAc;;AAEhB,QAAO;;;;;;;AAQT,SAAgB,sBAAsB,UAA6C;CACjF,MAAM,SAAoC,EAAE;CAE5C,MAAM,QAAQ,IAAI,aAAa,CAAC,OAAO,SAAS,CAAC;AACjD,KAAI,QAAA,OACF,QAAO,KAAK;EACV,UAAU;EACV,MAAM;EACN,SAAS,eAAe,MAAM,+CAA+C,mBAAmB;EACjG,CAAC;AAGJ,MAAK,MAAM,UAAU,yBACnB,KAAI,OAAO,QAAQ,KAAK,SAAS,CAC/B,QAAO,KAAK;EAAE,UAAU;EAAS,MAAM,OAAO;EAAM,SAAS,OAAO;EAAS,CAAC;AAIlF,KAAI,CAAC,wBAAwB,KAAK,SAAS,CACzC,QAAO,KAAK;EACV,UAAU;EACV,MAAM;EACN,SACE;EACH,CAAC;AAGJ,KAAI;AACF,kBAAgB,CAAC,MAAM,SAAS;UACzB,OAAO;AACd,SAAO,KAAK;GACV,UAAU;GACV,MAAM;GACN,SAAS,mCAAmC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;GACnG,CAAC;;AAGJ,QAAO"}
package/dist/validate.mjs CHANGED
@@ -114,7 +114,8 @@ function readFlow(flow) {
114
114
  const entry = isPlainObject(t) ? t : {};
115
115
  transitions.set(key, {
116
116
  target: str(entry.target),
117
- action: typeof entry.action === "string" ? entry.action : null
117
+ action: typeof entry.action === "string" ? entry.action : null,
118
+ purpose: typeof entry.purpose === "string" ? entry.purpose : null
118
119
  });
119
120
  }
120
121
  steps.push({
@@ -226,9 +227,18 @@ function bfs(start, adj, visited) {
226
227
  }
227
228
  function validateGraph(def, stepNames) {
228
229
  const issues = [];
229
- for (const step of def.steps) for (const [key, t] of step.transitions) if (isCurrentFlow(t)) {
230
- if (!stepNames.has(t.target)) issues.push(error("graph", `step ${q(step.name)}: transition ${q(key)} targets unknown step ${q(t.target)}`, step.name));
231
- } else if (t.action !== "switch" && t.action !== "pivot") issues.push(error("graph", `step ${q(step.name)}: transition ${q(key)} has invalid action ${q(t.action ?? "")}`, step.name));
230
+ for (const step of def.steps) for (const [key, t] of step.transitions) {
231
+ if (t.purpose !== null) if (t.action !== null) issues.push(error("graph", `step ${q(step.name)}: transition ${q(key)} declares both purpose and action; a transition either re-purposes locally or targets another flow`, step.name));
232
+ else if (!FLOW_PURPOSES.includes(t.purpose)) issues.push(error("graph", `step ${q(step.name)}: transition ${q(key)} has invalid purpose`, step.name));
233
+ else {
234
+ const entry = def.purposes.get(t.purpose);
235
+ if (entry === void 0) issues.push(error("graph", `step ${q(step.name)}: transition ${q(key)} re-purposes to ${q(t.purpose)}, which this definition does not serve`, step.name));
236
+ else if (t.target !== entry) issues.push(error("graph", `step ${q(step.name)}: transition ${q(key)} re-purposes to ${q(t.purpose)} but targets ${q(t.target)}; it must target that purpose's entry step ${q(entry)}`, step.name));
237
+ }
238
+ if (isCurrentFlow(t)) {
239
+ if (!stepNames.has(t.target)) issues.push(error("graph", `step ${q(step.name)}: transition ${q(key)} targets unknown step ${q(t.target)}`, step.name));
240
+ } else if (t.action !== "switch" && t.action !== "pivot") issues.push(error("graph", `step ${q(step.name)}: transition ${q(key)} has invalid action ${q(t.action ?? "")}`, step.name));
241
+ }
232
242
  for (const step of def.steps) if (!step.terminal && step.transitions.size === 0) issues.push(error("graph", `step ${q(step.name)} is non-terminal but has no outgoing transitions`, step.name));
233
243
  const reachable = /* @__PURE__ */ new Set();
234
244
  const adj = localAdjacency(def);
@@ -276,6 +286,38 @@ function schemaProperties(schema) {
276
286
  function schemaRequired(schema) {
277
287
  return Array.isArray(schema.required) ? schema.required.map(str).filter((n) => n !== "") : [];
278
288
  }
289
+ /**
290
+ * Mirrors `schemaReader.RequiredPaths`: every name in `required`,
291
+ * recursed through the nested `required` of each required object. A
292
+ * required object declaring no `required` of its own ends the descent at
293
+ * the object itself.
294
+ *
295
+ * An optional object contributes its own `required` too once it appears
296
+ * in `materialized`. It only exists in the document because something
297
+ * beneath it was collected, and from that point document validation
298
+ * enforces the rest of its `required` list.
299
+ */
300
+ function requiredPaths(schema, materialized, prefix = "") {
301
+ const properties = schemaProperties(schema);
302
+ const required = schemaRequired(schema);
303
+ const out = /* @__PURE__ */ new Set();
304
+ for (const name of required) {
305
+ const path = prefix === "" ? name : `${prefix}.${name}`;
306
+ const property = properties !== void 0 && Object.hasOwn(properties, name) ? properties[name] : void 0;
307
+ if (!isPlainObject(property) || schemaRequired(property).length === 0) {
308
+ out.add(path);
309
+ continue;
310
+ }
311
+ for (const nested of requiredPaths(property, materialized, path)) out.add(nested);
312
+ }
313
+ for (const [name, property] of Object.entries(properties ?? {})) {
314
+ if (required.includes(name) || !isPlainObject(property)) continue;
315
+ const path = prefix === "" ? name : `${prefix}.${name}`;
316
+ if (!materialized.has(path)) continue;
317
+ for (const nested of requiredPaths(property, materialized, path)) out.add(nested);
318
+ }
319
+ return [...out];
320
+ }
279
321
  /** Mirrors `xAuthMethodsReader.IsEnabled` in flow_field_resolver_schema.go. */
280
322
  function authMethodEnabled(schema, method) {
281
323
  if (!isPlainObject(schema["x-auth-methods"])) return false;
@@ -294,13 +336,24 @@ function resolveFieldChallenge(field, properties, schema) {
294
336
  if (!authMethodEnabled(schema, method)) return { message: `${q(method)} is not an enabled authentication method` };
295
337
  return { challenge: "password" };
296
338
  }
297
- const property = properties[field];
298
- if (property === void 0) return { message: `flow field: not a property in the user schema: ${q(field)}` };
339
+ const segments = field.split(".");
340
+ let property = void 0;
341
+ let level = properties;
342
+ for (const [i, segment] of segments.entries()) {
343
+ property = level !== void 0 && Object.hasOwn(level, segment) ? level[segment] : void 0;
344
+ if (property === void 0) return { message: `flow field: not a property in the user schema: ${q(field)}` };
345
+ const nested = isPlainObject(property) ? schemaProperties(property) : void 0;
346
+ if (i < segments.length - 1 && nested === void 0) return { message: `flow field: not a property in the user schema: ${q(field)}` };
347
+ level = nested;
348
+ }
299
349
  if (!isPlainObject(property)) return { challenge: null };
300
- const jsonType = property.type;
350
+ let jsonType = property.type;
301
351
  if (Array.isArray(jsonType)) {
302
- if (jsonType.filter((t) => t !== "null").length > 1) return { message: `flow field: unsupported JSON type: [${jsonType.map(str).join(" ")}]` };
352
+ const nonNull = jsonType.filter((t) => t !== "null");
353
+ if (nonNull.length > 1) return { message: `flow field: unsupported JSON type: [${jsonType.map(str).join(" ")}]` };
354
+ jsonType = nonNull[0];
303
355
  }
356
+ if (jsonType === "object" || jsonType === "array" || schemaProperties(property) !== void 0 || "items" in property) return { message: `flow field: not a scalar property, name a nested leaf instead: ${q(field)}` };
304
357
  const unique = property["x-unique"];
305
358
  if (unique === "project" || unique === "team") return { challenge: "identifier" };
306
359
  return { challenge: null };
@@ -313,9 +366,15 @@ function validateAgainstSchema(def, schema) {
313
366
  issues.push(error("schema/fields-resolve", "user schema has no properties"));
314
367
  return issues;
315
368
  }
316
- const collected = /* @__PURE__ */ new Set();
317
- for (const step of def.steps) for (const field of step.fields) collected.add(field);
318
- const missing = schemaRequired(raw).filter((field) => !collected.has(field)).sort();
369
+ const covered = /* @__PURE__ */ new Set();
370
+ for (const step of def.steps) for (const field of step.fields) {
371
+ let path = "";
372
+ for (const node of field.split(".")) {
373
+ path = path === "" ? node : `${path}.${node}`;
374
+ covered.add(path);
375
+ }
376
+ }
377
+ const missing = requiredPaths(raw, covered).filter((field) => !covered.has(field)).sort();
319
378
  if (missing.length > 0) issues.push(error("schema/required-fields", `required fields [${missing.join(" ")}] in user schema are missing in the flow definition steps`));
320
379
  const challengesByStep = /* @__PURE__ */ new Map();
321
380
  let resolutionFailed = false;
@@ -1 +1 @@
1
- {"version":3,"file":"validate.mjs","names":[],"sources":["../src/validate.ts"],"sourcesContent":["/**\n * Plan-time port of the server's flow-definition validator\n * (`internal/domain/flow_definition_validator.go`). The CLI runs it in\n * `buildSyncPlan` so every rule the server enforces on apply speaks up\n * at plan time — with the same words — before anything mutates.\n *\n * Contract with the Go source:\n *\n * - Every rule id in {@link FLOW_VALIDATION_RULES} names the Go function\n * it ports (`goRef`); a drift-audit test asserts those functions still\n * exist and that the shared literals below still match.\n * - Error messages mirror the Go detail strings verbatim, so a user who\n * bypasses plan sees identical text from the server.\n * - Unlike the Go validator (fail-fast), this port collects every issue.\n * Later phases are gated on earlier phases being clean so one defect\n * does not cascade into noise (a BFS over duplicate step names is\n * ill-defined).\n *\n * Scope cuts (server/service-side, not portable): pivot/switch targets\n * must name an active flow definition in the same project (needs the\n * DB); the user schema itself is assumed meta-schema-valid.\n *\n * Lifecycle: this port is a local-first interim, not a second source of\n * truth to grow. Its designed successor is the server-side validate-only\n * bundle endpoint (zitadel/nextgen#449); once plan can ask the server,\n * this file, its drift audit, and the keep-in-sync comment on the Go\n * validator all go away. New rules land in Go first and are ported here\n * only until then.\n */\n\nexport type FlowValidationSeverity = \"error\" | \"warning\";\n\nexport type FlowValidationIssue = {\n severity: FlowValidationSeverity;\n /** Stable rule id from {@link FLOW_VALIDATION_RULES}. */\n rule: FlowValidationRuleId;\n /** Mirrors the Go `ErrFlowDefinitionInvalid` detail string where a Go rule exists. */\n message: string;\n /** Step the issue is anchored to, when applicable. */\n step?: string;\n};\n\n/**\n * Rule registry: rule id → the Go function it ports. `goRef: null` marks\n * CLI-only guidance with no server counterpart.\n */\nexport const FLOW_VALIDATION_RULES = {\n \"step-names\": { goRef: \"stepNamesMap\" },\n definition: { goRef: \"validateDefinition\" },\n steps: { goRef: \"validateSteps\" },\n graph: { goRef: \"validateGraph\" },\n cycles: { goRef: \"validateCycles\" },\n \"flip-table\": { goRef: \"validateFlipTableCoverage\" },\n \"schema/required-fields\": { goRef: \"validateRequiredUserSchemaFields\" },\n \"schema/fields-resolve\": { goRef: \"resolveAllStepFields\" },\n \"schema/passkey-actions\": { goRef: \"validatePasskeyActionsEnabled\" },\n \"schema/on-success-manifest\": { goRef: \"validateOnSuccessManifests\" },\n \"warn/password-without-identifier\": { goRef: null },\n} as const;\n\nexport type FlowValidationRuleId = keyof typeof FLOW_VALIDATION_RULES;\n\n/** Mirrors `reservedOutcomes` in flow_definition_validator.go. */\nexport const RESERVED_OUTCOMES = [\"user_not_found\", \"user_already_exists\", \"callback\"] as const;\n\n/** Mirrors the `FlowDefinitionPurpose` enum (snake transform) in flow_definition.go. */\nexport const FLOW_PURPOSES = [\n \"login\",\n \"register\",\n \"recovery\",\n \"profiling\",\n \"reauth\",\n \"link_account\",\n] as const;\n\n/** Mirrors `purposeFlipTargets` in flow_definition_validator.go. */\nexport const PURPOSE_FLIP_TARGETS: Readonly<Record<string, Readonly<Record<string, string>>>> = {\n login: { user_not_found: \"register\" },\n register: { user_already_exists: \"login\" },\n};\n\n/** Mirrors `flipOutcomeImpacts` in flow_definition_validator.go (verbatim). */\nconst FLIP_OUTCOME_IMPACTS: Readonly<Record<string, string>> = {\n user_not_found:\n \"someone without an account gets stuck at sign-in instead of being routed to registration\",\n user_already_exists:\n \"someone who already has an account gets stuck at registration instead of being routed to sign-in\",\n};\n\n/** Action kinds a flow author may declare; `back` is engine-injected. */\nconst DECLARABLE_ACTION_KINDS = new Set([\"submit\", \"passkey\", \"passkey_register\", \"navigate\"]);\n\n/** Mirrors `flowBackActionName` in flow_state_machine.go. */\nconst BACK_ACTION_NAME = \"back\";\n\n/** Mirrors `authMethodPrefix` in flow_definition.go. */\nconst AUTH_METHOD_PREFIX = \"x-auth-methods#\";\n\n/** Mirrors `ManifestForOnSuccess` in flow_on_success.go. */\nconst ON_SUCCESS_MANIFESTS: Readonly<Record<string, readonly FieldChallenge[]>> = {\n create_user: [\"identifier\", \"password\"],\n};\n\ntype FieldChallenge = \"identifier\" | \"password\";\n\n/**\n * Validate a flow-definition config body (the `.zitadel/flows/*.json`\n * content) against the rules the server enforces on apply. `schema` is\n * the content of the user schema the flow's `user_schema` pins; when\n * omitted, schema-dependent rules are skipped. Pure — collects all\n * issues, never throws.\n */\nexport function validateFlowDefinition(flow: object, schema?: object): FlowValidationIssue[] {\n const def = readFlow(flow);\n const issues: FlowValidationIssue[] = [];\n\n // Phase 1 — step names (everything else keys off them).\n const stepNames = new Set<string>();\n for (const step of def.steps) {\n if (stepNames.has(step.name)) {\n issues.push(error(\"step-names\", `duplicate step name ${q(step.name)}`, step.name));\n }\n stepNames.add(step.name);\n }\n\n // Phase 2 — purposes.\n if (def.purposes.size === 0) {\n issues.push(error(\"definition\", \"no purposes defined\"));\n }\n for (const [purpose, entryStep] of def.purposes) {\n if (!(FLOW_PURPOSES as readonly string[]).includes(purpose)) {\n issues.push(error(\"definition\", `'${purpose}' is not a valid purpose`));\n continue;\n }\n if (entryStep === \"\") {\n issues.push(error(\"definition\", `initial step for purpose '${purpose}' is empty`));\n continue;\n }\n if (!stepNames.has(entryStep)) {\n issues.push(\n error(\"definition\", `purpose ${q(purpose)} targets unknown entry-point step ${q(entryStep)}`),\n );\n }\n }\n\n // Phase 3 — per-step structure.\n for (const step of def.steps) {\n issues.push(...validateStep(step));\n }\n\n // Graph phases are ill-defined over broken structure — gate on 1–3.\n if (issues.length === 0) {\n issues.push(...validateGraph(def, stepNames));\n if (issues.length === 0) {\n issues.push(...validateCycles(def));\n }\n issues.push(...validateFlipTableCoverage(def));\n }\n\n // Schema-dependent phases: only meaningful over structurally sound\n // steps, and only when the paired schema content is available.\n if (schema !== undefined && issues.length === 0) {\n issues.push(...validateAgainstSchema(def, schema));\n }\n\n return issues;\n}\n\n// ── internal flow reading ────────────────────────────────────────────────────\n\ntype FlowStep = {\n name: string;\n fields: string[];\n actions: { name: string; kind: string }[];\n gateCount: number;\n ssoProviderCount: number;\n transitions: Map<string, { target: string; action: string | null }>;\n onSuccess: string | undefined;\n terminal: boolean;\n};\n\ntype FlowDef = {\n purposes: Map<string, string>;\n steps: FlowStep[];\n};\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction str(value: unknown): string {\n return typeof value === \"string\" ? value : \"\";\n}\n\n/**\n * Coerce raw parsed JSON into the internal shape. Callers usually run\n * the Zod config schema first; the defensive reads keep the standalone\n * API from crashing on malformed input (it reports rule violations on\n * whatever is readable instead).\n */\nfunction readFlow(flow: object): FlowDef {\n const raw = flow as Record<string, unknown>;\n\n const purposes = new Map<string, string>();\n if (isPlainObject(raw.purposes)) {\n for (const [purpose, entry] of Object.entries(raw.purposes)) {\n purposes.set(purpose, str(entry));\n }\n }\n\n const steps: FlowStep[] = [];\n if (Array.isArray(raw.steps)) {\n for (const value of raw.steps) {\n if (!isPlainObject(value)) continue;\n const transitions = new Map<string, { target: string; action: string | null }>();\n if (isPlainObject(value.transitions)) {\n for (const [key, t] of Object.entries(value.transitions)) {\n const entry = isPlainObject(t) ? t : {};\n transitions.set(key, {\n target: str(entry.target),\n action: typeof entry.action === \"string\" ? entry.action : null,\n });\n }\n }\n steps.push({\n name: str(value.name),\n fields: Array.isArray(value.fields) ? value.fields.map(str) : [],\n actions: Array.isArray(value.actions)\n ? value.actions\n .filter(isPlainObject)\n .map((a) => ({ name: str(a.name), kind: str(a.kind) }))\n : [],\n gateCount: isPlainObject(value.gates) ? Object.keys(value.gates).length : 0,\n ssoProviderCount: Array.isArray(value.sso_providers) ? value.sso_providers.length : 0,\n transitions,\n onSuccess: typeof value.on_success === \"string\" ? value.on_success : undefined,\n terminal: value.complete !== undefined && value.complete !== null,\n });\n }\n }\n\n return { purposes, steps };\n}\n\n/** Go `%q` — double-quoted string. */\nfunction q(value: string): string {\n return JSON.stringify(value);\n}\n\nfunction error(rule: FlowValidationRuleId, message: string, step?: string): FlowValidationIssue {\n return step === undefined\n ? { severity: \"error\", rule, message }\n : { severity: \"error\", rule, message, step };\n}\n\n// ── phase 3: per-step structure (validateSteps + uniqueNonEmptyFields) ──────\n\nfunction validateStep(step: FlowStep): FlowValidationIssue[] {\n const issues: FlowValidationIssue[] = [];\n const name = step.name;\n\n if (step.terminal) {\n if (\n step.fields.length > 0 ||\n step.actions.length > 0 ||\n step.transitions.size > 0 ||\n step.gateCount > 0 ||\n step.ssoProviderCount > 0\n ) {\n issues.push(\n error(\n \"steps\",\n `step ${q(name)} is terminal (complete is set) but has fields, actions, transitions, gates, or sso_providers`,\n name,\n ),\n );\n }\n return issues;\n }\n\n const seenFields = new Set<string>();\n for (const field of step.fields) {\n if (field === \"\") {\n issues.push(error(\"steps\", `step ${q(name)}: field entry is empty`, name));\n continue;\n }\n if (seenFields.has(field)) {\n issues.push(error(\"steps\", `step ${q(name)}: duplicate field ${q(field)}`, name));\n }\n seenFields.add(field);\n }\n\n const actionNames = new Set<string>();\n for (const action of step.actions) {\n if (action.name === \"\") {\n issues.push(error(\"steps\", `step ${q(name)}: action has empty name`, name));\n continue;\n }\n if (actionNames.has(action.name)) {\n issues.push(error(\"steps\", `step ${q(name)}: duplicate action ${q(action.name)}`, name));\n continue;\n }\n if (action.kind === \"\" || (action.kind !== \"back\" && !DECLARABLE_ACTION_KINDS.has(action.kind))) {\n issues.push(error(\"steps\", `step ${q(name)}: action ${q(action.name)} has no kind`, name));\n } else if (action.kind === \"back\") {\n issues.push(\n error(\n \"steps\",\n `step ${q(name)}: action ${q(action.name)} has kind=back, which is engine-injected and cannot be declared`,\n name,\n ),\n );\n }\n if (action.name === BACK_ACTION_NAME) {\n issues.push(\n error(\n \"steps\",\n `step ${q(name)}: action name ${q(action.name)} is reserved for engine-injected back navigation`,\n name,\n ),\n );\n }\n actionNames.add(action.name);\n }\n\n const hasCallback = step.transitions.has(\"callback\");\n if (\n step.fields.length === 0 &&\n step.actions.length === 0 &&\n step.ssoProviderCount === 0 &&\n step.gateCount === 0 &&\n !hasCallback\n ) {\n issues.push(\n error(\n \"steps\",\n `step ${q(name)} is non-terminal but has no fields, actions, sso_providers, gates, or transitions.callback`,\n name,\n ),\n );\n }\n\n if (step.ssoProviderCount > 0 && !hasCallback) {\n issues.push(\n error(\"steps\", `step ${q(name)}: has sso_providers but is missing transitions.callback`, name),\n );\n }\n\n for (const actionName of actionNames) {\n if (!step.transitions.has(actionName)) {\n issues.push(\n error(\"steps\", `step ${q(name)}: action ${q(actionName)} has no matching transition`, name),\n );\n }\n }\n\n for (const transitionKey of step.transitions.keys()) {\n if (!actionNames.has(transitionKey) && !(RESERVED_OUTCOMES as readonly string[]).includes(transitionKey)) {\n issues.push(\n error(\n \"steps\",\n `step ${q(name)}: transition key ${q(transitionKey)} is not an action name or reserved outcome (user_not_found, user_already_exists, callback)`,\n name,\n ),\n );\n }\n }\n\n return issues;\n}\n\n// ── phase 4: graph / cycles / flip table ─────────────────────────────────────\n\n/** A transition with no cross-flow action targets a step in this flow. */\nfunction isCurrentFlow(t: { action: string | null }): boolean {\n return t.action === null;\n}\n\n/** Adjacency over local (current-flow) transitions: step → target steps. */\nfunction localAdjacency(def: FlowDef): Map<string, string[]> {\n const adj = new Map<string, string[]>();\n for (const step of def.steps) {\n for (const t of step.transitions.values()) {\n if (isCurrentFlow(t)) {\n const targets = adj.get(step.name) ?? [];\n targets.push(t.target);\n adj.set(step.name, targets);\n }\n }\n }\n return adj;\n}\n\n/** Reverse adjacency over local transitions: target step → source steps. */\nfunction reverseAdjacency(def: FlowDef): Map<string, string[]> {\n const reverse = new Map<string, string[]>();\n for (const step of def.steps) {\n for (const t of step.transitions.values()) {\n if (isCurrentFlow(t)) {\n const sources = reverse.get(t.target) ?? [];\n sources.push(step.name);\n reverse.set(t.target, sources);\n }\n }\n }\n return reverse;\n}\n\n/** BFS from `start` over `adj`, accumulating into `visited`. */\nfunction bfs(start: string, adj: Map<string, string[]>, visited: Set<string>): void {\n const queue = [start];\n for (let head = 0; head < queue.length; head += 1) {\n const current = queue[head];\n if (current === undefined || visited.has(current)) continue;\n visited.add(current);\n queue.push(...(adj.get(current) ?? []));\n }\n}\n\nfunction validateGraph(def: FlowDef, stepNames: Set<string>): FlowValidationIssue[] {\n const issues: FlowValidationIssue[] = [];\n\n for (const step of def.steps) {\n for (const [key, t] of step.transitions) {\n if (isCurrentFlow(t)) {\n if (!stepNames.has(t.target)) {\n issues.push(\n error(\n \"graph\",\n `step ${q(step.name)}: transition ${q(key)} targets unknown step ${q(t.target)}`,\n step.name,\n ),\n );\n }\n } else if (t.action !== \"switch\" && t.action !== \"pivot\") {\n issues.push(\n error(\n \"graph\",\n `step ${q(step.name)}: transition ${q(key)} has invalid action ${q(t.action ?? \"\")}`,\n step.name,\n ),\n );\n }\n }\n }\n\n for (const step of def.steps) {\n if (!step.terminal && step.transitions.size === 0) {\n issues.push(\n error(\"graph\", `step ${q(step.name)} is non-terminal but has no outgoing transitions`, step.name),\n );\n }\n }\n\n const reachable = new Set<string>();\n const adj = localAdjacency(def);\n for (const entryStep of def.purposes.values()) {\n bfs(entryStep, adj, reachable);\n }\n for (const step of def.steps) {\n if (!reachable.has(step.name)) {\n issues.push(error(\"graph\", `step ${q(step.name)} is unreachable from any entry point`, step.name));\n }\n }\n\n return issues;\n}\n\nfunction validateCycles(def: FlowDef): FlowValidationIssue[] {\n const issues: FlowValidationIssue[] = [];\n const reverse = reverseAdjacency(def);\n\n // Seed: escape nodes (terminal, or a switch/pivot out); BFS backwards.\n const safe = new Set<string>();\n const queue: string[] = [];\n for (const step of def.steps) {\n const escapes =\n step.terminal || [...step.transitions.values()].some((t) => !isCurrentFlow(t));\n if (escapes) {\n safe.add(step.name);\n queue.push(step.name);\n }\n }\n for (let head = 0; head < queue.length; head += 1) {\n const current = queue[head];\n if (current === undefined) continue;\n for (const pred of reverse.get(current) ?? []) {\n if (!safe.has(pred)) {\n safe.add(pred);\n queue.push(pred);\n }\n }\n }\n\n for (const step of def.steps) {\n if (!step.terminal && !safe.has(step.name)) {\n issues.push(\n error(\n \"cycles\",\n `step ${q(step.name)} is trapped: no path to a terminal step or another flow`,\n step.name,\n ),\n );\n }\n }\n return issues;\n}\n\nfunction validateFlipTableCoverage(def: FlowDef): FlowValidationIssue[] {\n const issues: FlowValidationIssue[] = [];\n for (const [purpose, entryStepName] of def.purposes) {\n const flipTargets = PURPOSE_FLIP_TARGETS[purpose];\n if (flipTargets === undefined) continue;\n const entry = def.steps.find((s) => s.name === entryStepName);\n if (entry === undefined) continue;\n for (const [outcome, targetPurpose] of Object.entries(flipTargets)) {\n if (!def.purposes.has(targetPurpose)) continue;\n if (!entry.transitions.has(outcome)) {\n issues.push(\n error(\n \"flip-table\",\n `step ${q(entry.name)}: entry step for purpose ${q(purpose)} must wire ${q(outcome)} transition because ${q(targetPurpose)} is also a purpose: without it, ${FLIP_OUTCOME_IMPACTS[outcome]}`,\n entry.name,\n ),\n );\n }\n }\n }\n return issues;\n}\n\n// ── phase 5: schema-dependent rules ──────────────────────────────────────────\n\nfunction schemaProperties(schema: Record<string, unknown>): Record<string, unknown> | undefined {\n return isPlainObject(schema.properties) ? schema.properties : undefined;\n}\n\nfunction schemaRequired(schema: Record<string, unknown>): string[] {\n return Array.isArray(schema.required) ? schema.required.map(str).filter((n) => n !== \"\") : [];\n}\n\n/** Mirrors `xAuthMethodsReader.IsEnabled` in flow_field_resolver_schema.go. */\nfunction authMethodEnabled(schema: Record<string, unknown>, method: string): boolean {\n if (!isPlainObject(schema[\"x-auth-methods\"])) return false;\n const entry = schema[\"x-auth-methods\"][method];\n return isPlainObject(entry) && entry.enabled === true;\n}\n\n/**\n * Resolve one step field to its challenge, mirroring the subset of\n * `SchemaFieldResolver.Resolve` the validator needs. Returns either the\n * challenge (or null for none) or the Go-verbatim resolution error.\n */\nfunction resolveFieldChallenge(\n field: string,\n properties: Record<string, unknown>,\n schema: Record<string, unknown>,\n): { challenge: FieldChallenge | null } | { message: string } {\n if (field.startsWith(AUTH_METHOD_PREFIX)) {\n const method = field.slice(AUTH_METHOD_PREFIX.length);\n // Mirrors deriveAuthMethodType: only password is field-shaped today.\n if (method !== \"password\") {\n return { message: `unknown field type for ${q(field)}` };\n }\n if (!authMethodEnabled(schema, method)) {\n // Surfaced by resolveAllStepFields' enabled-method cross-check.\n return { message: `${q(method)} is not an enabled authentication method` };\n }\n return { challenge: \"password\" };\n }\n\n const property = properties[field];\n if (property === undefined) {\n return { message: `flow field: not a property in the user schema: ${q(field)}` };\n }\n if (!isPlainObject(property)) {\n return { challenge: null };\n }\n\n // Mirrors schemaReader.JSONType: the nullable idiom `[\"null\", X]`\n // reduces to X; any other multi-entry union is unsupported.\n const jsonType = property.type;\n if (Array.isArray(jsonType)) {\n const nonNull = jsonType.filter((t) => t !== \"null\");\n if (nonNull.length > 1) {\n return { message: `flow field: unsupported JSON type: [${jsonType.map(str).join(\" \")}]` };\n }\n }\n\n // Mirrors deriveUnique + deriveIdentifierChallenge: any recognized\n // non-empty x-unique scope makes the property an identifier.\n const unique = property[\"x-unique\"];\n if (unique === \"project\" || unique === \"team\") {\n return { challenge: \"identifier\" };\n }\n return { challenge: null };\n}\n\nfunction validateAgainstSchema(def: FlowDef, schema: object): FlowValidationIssue[] {\n const issues: FlowValidationIssue[] = [];\n const raw = schema as Record<string, unknown>;\n\n const properties = schemaProperties(raw);\n if (properties === undefined) {\n issues.push(error(\"schema/fields-resolve\", \"user schema has no properties\"));\n return issues;\n }\n\n // Required-fields coverage (validateRequiredUserSchemaFields).\n const collected = new Set<string>();\n for (const step of def.steps) {\n for (const field of step.fields) {\n collected.add(field);\n }\n }\n const missing = schemaRequired(raw)\n .filter((field) => !collected.has(field))\n .sort();\n if (missing.length > 0) {\n issues.push(\n error(\n \"schema/required-fields\",\n `required fields [${missing.join(\" \")}] in user schema are missing in the flow definition steps`,\n ),\n );\n }\n\n // Field resolution (resolveAllStepFields), collecting challenges for\n // the manifest and warning rules below.\n const challengesByStep = new Map<string, Set<FieldChallenge>>();\n let resolutionFailed = false;\n for (const step of def.steps) {\n const challenges = new Set<FieldChallenge>();\n for (const field of step.fields) {\n const resolved = resolveFieldChallenge(field, properties, raw);\n if (\"message\" in resolved) {\n issues.push(\n error(\"schema/fields-resolve\", `step ${q(step.name)}: ${resolved.message}`, step.name),\n );\n resolutionFailed = true;\n continue;\n }\n if (resolved.challenge !== null) {\n challenges.add(resolved.challenge);\n }\n }\n challengesByStep.set(step.name, challenges);\n }\n\n // Passkey enablement (validatePasskeyActionsEnabled): passkey is\n // action-shaped, not field-shaped, so the per-field enabled-method\n // check above never sees it.\n if (!authMethodEnabled(raw, \"passkey\")) {\n for (const step of def.steps) {\n for (const action of step.actions) {\n if (action.kind === \"passkey\" || action.kind === \"passkey_register\") {\n issues.push(\n error(\n \"schema/passkey-actions\",\n `step ${q(step.name)}: action ${q(action.name)} offers passkey but \"passkey\" is not an enabled authentication method`,\n step.name,\n ),\n );\n }\n }\n }\n }\n\n if (resolutionFailed) {\n return issues;\n }\n\n // on_success manifests (validateOnSuccessManifests): every kind the\n // mutation needs must be collected on the step or upstream.\n const reverse = reverseAdjacency(def);\n for (const step of def.steps) {\n if (step.onSuccess === undefined) continue;\n const manifest = ON_SUCCESS_MANIFESTS[step.onSuccess];\n if (manifest === undefined) continue;\n const upstream = new Set<string>();\n bfs(step.name, reverse, upstream);\n for (const kind of manifest) {\n const established = [...upstream].some((name) => challengesByStep.get(name)?.has(kind));\n if (!established) {\n issues.push(\n error(\n \"schema/on-success-manifest\",\n `step ${q(step.name)}: on_success ${step.onSuccess} requires ${q(kind)} to be collected upstream`,\n step.name,\n ),\n );\n }\n }\n }\n\n // CLI-only guidance (no Go counterpart): a password collected on the\n // login path with no identifier upstream leaves the engine unable to\n // resolve which user to challenge. Path-sensitive: walk forward from\n // the login entry and stop at any step that collects an identifier\n // (every route through it is covered), so an identifier on a\n // *different* route — e.g. only on the register-purpose chain into a\n // shared step — cannot mask a login route that never collects one.\n const loginEntry = def.purposes.get(\"login\");\n if (loginEntry !== undefined) {\n const adj = localAdjacency(def);\n const identifierFree = new Set<string>();\n const queue = [loginEntry];\n for (let head = 0; head < queue.length; head += 1) {\n const name = queue[head];\n if (name === undefined || identifierFree.has(name)) continue;\n if (challengesByStep.get(name)?.has(\"identifier\")) continue;\n identifierFree.add(name);\n queue.push(...(adj.get(name) ?? []));\n }\n for (const step of def.steps) {\n if (!identifierFree.has(step.name)) continue;\n if (!challengesByStep.get(step.name)?.has(\"password\")) continue;\n issues.push({\n severity: \"warning\",\n rule: \"warn/password-without-identifier\",\n message: `step ${q(step.name)} collects ${AUTH_METHOD_PREFIX}password on the login path but no upstream step collects an identifier field — the engine cannot resolve which user to challenge`,\n step: step.name,\n });\n }\n }\n\n return issues;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"validate.mjs","names":[],"sources":["../src/validate.ts"],"sourcesContent":["/**\n * Plan-time port of the server's flow-definition validator\n * (`internal/domain/flow_definition_validator.go`). The CLI runs it in\n * `buildSyncPlan` so every rule the server enforces on apply speaks up\n * at plan time — with the same words — before anything mutates.\n *\n * Contract with the Go source:\n *\n * - Every rule id in {@link FLOW_VALIDATION_RULES} names the Go function\n * it ports (`goRef`); a drift-audit test asserts those functions still\n * exist and that the shared literals below still match.\n * - Error messages mirror the Go detail strings verbatim, so a user who\n * bypasses plan sees identical text from the server.\n * - Unlike the Go validator (fail-fast), this port collects every issue.\n * Later phases are gated on earlier phases being clean so one defect\n * does not cascade into noise (a BFS over duplicate step names is\n * ill-defined).\n *\n * Scope cuts (server/service-side, not portable): pivot/switch targets\n * must name an active flow definition in the same project (needs the\n * DB); the user schema itself is assumed meta-schema-valid.\n *\n * Lifecycle: this port is a local-first interim, not a second source of\n * truth to grow. Its designed successor is the server-side validate-only\n * bundle endpoint (zitadel/nextgen#449); once plan can ask the server,\n * this file, its drift audit, and the keep-in-sync comment on the Go\n * validator all go away. New rules land in Go first and are ported here\n * only until then.\n */\n\nexport type FlowValidationSeverity = \"error\" | \"warning\";\n\nexport type FlowValidationIssue = {\n severity: FlowValidationSeverity;\n /** Stable rule id from {@link FLOW_VALIDATION_RULES}. */\n rule: FlowValidationRuleId;\n /** Mirrors the Go `ErrFlowDefinitionInvalid` detail string where a Go rule exists. */\n message: string;\n /** Step the issue is anchored to, when applicable. */\n step?: string;\n};\n\n/**\n * Rule registry: rule id → the Go function it ports. `goRef: null` marks\n * CLI-only guidance with no server counterpart.\n */\nexport const FLOW_VALIDATION_RULES = {\n \"step-names\": { goRef: \"stepNamesMap\" },\n definition: { goRef: \"validateDefinition\" },\n steps: { goRef: \"validateSteps\" },\n graph: { goRef: \"validateGraph\" },\n cycles: { goRef: \"validateCycles\" },\n \"flip-table\": { goRef: \"validateFlipTableCoverage\" },\n \"schema/required-fields\": { goRef: \"validateRequiredUserSchemaFields\" },\n \"schema/fields-resolve\": { goRef: \"resolveAllStepFields\" },\n \"schema/passkey-actions\": { goRef: \"validatePasskeyActionsEnabled\" },\n \"schema/on-success-manifest\": { goRef: \"validateOnSuccessManifests\" },\n \"warn/password-without-identifier\": { goRef: null },\n} as const;\n\nexport type FlowValidationRuleId = keyof typeof FLOW_VALIDATION_RULES;\n\n/** Mirrors `reservedOutcomes` in flow_definition_validator.go. */\nexport const RESERVED_OUTCOMES = [\"user_not_found\", \"user_already_exists\", \"callback\"] as const;\n\n/** Mirrors the `FlowDefinitionPurpose` enum (snake transform) in flow_definition.go. */\nexport const FLOW_PURPOSES = [\n \"login\",\n \"register\",\n \"recovery\",\n \"profiling\",\n \"reauth\",\n \"link_account\",\n] as const;\n\n/** Mirrors `purposeFlipTargets` in flow_definition_validator.go. */\nexport const PURPOSE_FLIP_TARGETS: Readonly<Record<string, Readonly<Record<string, string>>>> = {\n login: { user_not_found: \"register\" },\n register: { user_already_exists: \"login\" },\n};\n\n/** Mirrors `flipOutcomeImpacts` in flow_definition_validator.go (verbatim). */\nconst FLIP_OUTCOME_IMPACTS: Readonly<Record<string, string>> = {\n user_not_found:\n \"someone without an account gets stuck at sign-in instead of being routed to registration\",\n user_already_exists:\n \"someone who already has an account gets stuck at registration instead of being routed to sign-in\",\n};\n\n/** Action kinds a flow author may declare; `back` is engine-injected. */\nconst DECLARABLE_ACTION_KINDS = new Set([\"submit\", \"passkey\", \"passkey_register\", \"navigate\"]);\n\n/** Mirrors `flowBackActionName` in flow_state_machine.go. */\nconst BACK_ACTION_NAME = \"back\";\n\n/** Mirrors `authMethodPrefix` in flow_definition.go. */\nconst AUTH_METHOD_PREFIX = \"x-auth-methods#\";\n\n/** Mirrors `ManifestForOnSuccess` in flow_on_success.go. */\nconst ON_SUCCESS_MANIFESTS: Readonly<Record<string, readonly FieldChallenge[]>> = {\n create_user: [\"identifier\", \"password\"],\n};\n\ntype FieldChallenge = \"identifier\" | \"password\";\n\n/**\n * Validate a flow-definition config body (the `.zitadel/flows/*.json`\n * content) against the rules the server enforces on apply. `schema` is\n * the content of the user schema the flow's `user_schema` pins; when\n * omitted, schema-dependent rules are skipped. Pure — collects all\n * issues, never throws.\n */\nexport function validateFlowDefinition(flow: object, schema?: object): FlowValidationIssue[] {\n const def = readFlow(flow);\n const issues: FlowValidationIssue[] = [];\n\n // Phase 1 — step names (everything else keys off them).\n const stepNames = new Set<string>();\n for (const step of def.steps) {\n if (stepNames.has(step.name)) {\n issues.push(error(\"step-names\", `duplicate step name ${q(step.name)}`, step.name));\n }\n stepNames.add(step.name);\n }\n\n // Phase 2 — purposes.\n if (def.purposes.size === 0) {\n issues.push(error(\"definition\", \"no purposes defined\"));\n }\n for (const [purpose, entryStep] of def.purposes) {\n if (!(FLOW_PURPOSES as readonly string[]).includes(purpose)) {\n issues.push(error(\"definition\", `'${purpose}' is not a valid purpose`));\n continue;\n }\n if (entryStep === \"\") {\n issues.push(error(\"definition\", `initial step for purpose '${purpose}' is empty`));\n continue;\n }\n if (!stepNames.has(entryStep)) {\n issues.push(\n error(\"definition\", `purpose ${q(purpose)} targets unknown entry-point step ${q(entryStep)}`),\n );\n }\n }\n\n // Phase 3 — per-step structure.\n for (const step of def.steps) {\n issues.push(...validateStep(step));\n }\n\n // Graph phases are ill-defined over broken structure — gate on 1–3.\n if (issues.length === 0) {\n issues.push(...validateGraph(def, stepNames));\n if (issues.length === 0) {\n issues.push(...validateCycles(def));\n }\n issues.push(...validateFlipTableCoverage(def));\n }\n\n // Schema-dependent phases: only meaningful over structurally sound\n // steps, and only when the paired schema content is available.\n if (schema !== undefined && issues.length === 0) {\n issues.push(...validateAgainstSchema(def, schema));\n }\n\n return issues;\n}\n\n// ── internal flow reading ────────────────────────────────────────────────────\n\ntype FlowStep = {\n name: string;\n fields: string[];\n actions: { name: string; kind: string }[];\n gateCount: number;\n ssoProviderCount: number;\n transitions: Map<string, { target: string; action: string | null; purpose: string | null }>;\n onSuccess: string | undefined;\n terminal: boolean;\n};\n\ntype FlowDef = {\n purposes: Map<string, string>;\n steps: FlowStep[];\n};\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction str(value: unknown): string {\n return typeof value === \"string\" ? value : \"\";\n}\n\n/**\n * Coerce raw parsed JSON into the internal shape. Callers usually run\n * the Zod config schema first; the defensive reads keep the standalone\n * API from crashing on malformed input (it reports rule violations on\n * whatever is readable instead).\n */\nfunction readFlow(flow: object): FlowDef {\n const raw = flow as Record<string, unknown>;\n\n const purposes = new Map<string, string>();\n if (isPlainObject(raw.purposes)) {\n for (const [purpose, entry] of Object.entries(raw.purposes)) {\n purposes.set(purpose, str(entry));\n }\n }\n\n const steps: FlowStep[] = [];\n if (Array.isArray(raw.steps)) {\n for (const value of raw.steps) {\n if (!isPlainObject(value)) continue;\n const transitions = new Map<\n string,\n { target: string; action: string | null; purpose: string | null }\n >();\n if (isPlainObject(value.transitions)) {\n for (const [key, t] of Object.entries(value.transitions)) {\n const entry = isPlainObject(t) ? t : {};\n transitions.set(key, {\n target: str(entry.target),\n action: typeof entry.action === \"string\" ? entry.action : null,\n purpose: typeof entry.purpose === \"string\" ? entry.purpose : null,\n });\n }\n }\n steps.push({\n name: str(value.name),\n fields: Array.isArray(value.fields) ? value.fields.map(str) : [],\n actions: Array.isArray(value.actions)\n ? value.actions\n .filter(isPlainObject)\n .map((a) => ({ name: str(a.name), kind: str(a.kind) }))\n : [],\n gateCount: isPlainObject(value.gates) ? Object.keys(value.gates).length : 0,\n ssoProviderCount: Array.isArray(value.sso_providers) ? value.sso_providers.length : 0,\n transitions,\n onSuccess: typeof value.on_success === \"string\" ? value.on_success : undefined,\n terminal: value.complete !== undefined && value.complete !== null,\n });\n }\n }\n\n return { purposes, steps };\n}\n\n/** Go `%q` — double-quoted string. */\nfunction q(value: string): string {\n return JSON.stringify(value);\n}\n\nfunction error(rule: FlowValidationRuleId, message: string, step?: string): FlowValidationIssue {\n return step === undefined\n ? { severity: \"error\", rule, message }\n : { severity: \"error\", rule, message, step };\n}\n\n// ── phase 3: per-step structure (validateSteps + uniqueNonEmptyFields) ──────\n\nfunction validateStep(step: FlowStep): FlowValidationIssue[] {\n const issues: FlowValidationIssue[] = [];\n const name = step.name;\n\n if (step.terminal) {\n if (\n step.fields.length > 0 ||\n step.actions.length > 0 ||\n step.transitions.size > 0 ||\n step.gateCount > 0 ||\n step.ssoProviderCount > 0\n ) {\n issues.push(\n error(\n \"steps\",\n `step ${q(name)} is terminal (complete is set) but has fields, actions, transitions, gates, or sso_providers`,\n name,\n ),\n );\n }\n return issues;\n }\n\n const seenFields = new Set<string>();\n for (const field of step.fields) {\n if (field === \"\") {\n issues.push(error(\"steps\", `step ${q(name)}: field entry is empty`, name));\n continue;\n }\n if (seenFields.has(field)) {\n issues.push(error(\"steps\", `step ${q(name)}: duplicate field ${q(field)}`, name));\n }\n seenFields.add(field);\n }\n\n const actionNames = new Set<string>();\n for (const action of step.actions) {\n if (action.name === \"\") {\n issues.push(error(\"steps\", `step ${q(name)}: action has empty name`, name));\n continue;\n }\n if (actionNames.has(action.name)) {\n issues.push(error(\"steps\", `step ${q(name)}: duplicate action ${q(action.name)}`, name));\n continue;\n }\n if (action.kind === \"\" || (action.kind !== \"back\" && !DECLARABLE_ACTION_KINDS.has(action.kind))) {\n issues.push(error(\"steps\", `step ${q(name)}: action ${q(action.name)} has no kind`, name));\n } else if (action.kind === \"back\") {\n issues.push(\n error(\n \"steps\",\n `step ${q(name)}: action ${q(action.name)} has kind=back, which is engine-injected and cannot be declared`,\n name,\n ),\n );\n }\n if (action.name === BACK_ACTION_NAME) {\n issues.push(\n error(\n \"steps\",\n `step ${q(name)}: action name ${q(action.name)} is reserved for engine-injected back navigation`,\n name,\n ),\n );\n }\n actionNames.add(action.name);\n }\n\n const hasCallback = step.transitions.has(\"callback\");\n if (\n step.fields.length === 0 &&\n step.actions.length === 0 &&\n step.ssoProviderCount === 0 &&\n step.gateCount === 0 &&\n !hasCallback\n ) {\n issues.push(\n error(\n \"steps\",\n `step ${q(name)} is non-terminal but has no fields, actions, sso_providers, gates, or transitions.callback`,\n name,\n ),\n );\n }\n\n if (step.ssoProviderCount > 0 && !hasCallback) {\n issues.push(\n error(\"steps\", `step ${q(name)}: has sso_providers but is missing transitions.callback`, name),\n );\n }\n\n for (const actionName of actionNames) {\n if (!step.transitions.has(actionName)) {\n issues.push(\n error(\"steps\", `step ${q(name)}: action ${q(actionName)} has no matching transition`, name),\n );\n }\n }\n\n for (const transitionKey of step.transitions.keys()) {\n if (!actionNames.has(transitionKey) && !(RESERVED_OUTCOMES as readonly string[]).includes(transitionKey)) {\n issues.push(\n error(\n \"steps\",\n `step ${q(name)}: transition key ${q(transitionKey)} is not an action name or reserved outcome (user_not_found, user_already_exists, callback)`,\n name,\n ),\n );\n }\n }\n\n return issues;\n}\n\n// ── phase 4: graph / cycles / flip table ─────────────────────────────────────\n\n/** A transition with no cross-flow action targets a step in this flow. */\nfunction isCurrentFlow(t: { action: string | null }): boolean {\n return t.action === null;\n}\n\n/** Adjacency over local (current-flow) transitions: step → target steps. */\nfunction localAdjacency(def: FlowDef): Map<string, string[]> {\n const adj = new Map<string, string[]>();\n for (const step of def.steps) {\n for (const t of step.transitions.values()) {\n if (isCurrentFlow(t)) {\n const targets = adj.get(step.name) ?? [];\n targets.push(t.target);\n adj.set(step.name, targets);\n }\n }\n }\n return adj;\n}\n\n/** Reverse adjacency over local transitions: target step → source steps. */\nfunction reverseAdjacency(def: FlowDef): Map<string, string[]> {\n const reverse = new Map<string, string[]>();\n for (const step of def.steps) {\n for (const t of step.transitions.values()) {\n if (isCurrentFlow(t)) {\n const sources = reverse.get(t.target) ?? [];\n sources.push(step.name);\n reverse.set(t.target, sources);\n }\n }\n }\n return reverse;\n}\n\n/** BFS from `start` over `adj`, accumulating into `visited`. */\nfunction bfs(start: string, adj: Map<string, string[]>, visited: Set<string>): void {\n const queue = [start];\n for (let head = 0; head < queue.length; head += 1) {\n const current = queue[head];\n if (current === undefined || visited.has(current)) continue;\n visited.add(current);\n queue.push(...(adj.get(current) ?? []));\n }\n}\n\nfunction validateGraph(def: FlowDef, stepNames: Set<string>): FlowValidationIssue[] {\n const issues: FlowValidationIssue[] = [];\n\n for (const step of def.steps) {\n for (const [key, t] of step.transitions) {\n if (t.purpose !== null) {\n // Local re-purposing: never combined with a cross-flow action,\n // only to a purpose this definition serves, and only to that\n // purpose's entry step. Mirrors the server-side validator.\n if (t.action !== null) {\n issues.push(\n error(\n \"graph\",\n `step ${q(step.name)}: transition ${q(key)} declares both purpose and action; a transition either re-purposes locally or targets another flow`,\n step.name,\n ),\n );\n } else if (!(FLOW_PURPOSES as readonly string[]).includes(t.purpose)) {\n // Verbatim-identical to the Go validator's message (which cannot\n // print the raw value — its parse layer rejects it earlier).\n issues.push(\n error(\n \"graph\",\n `step ${q(step.name)}: transition ${q(key)} has invalid purpose`,\n step.name,\n ),\n );\n } else {\n const entry = def.purposes.get(t.purpose);\n if (entry === undefined) {\n issues.push(\n error(\n \"graph\",\n `step ${q(step.name)}: transition ${q(key)} re-purposes to ${q(t.purpose)}, which this definition does not serve`,\n step.name,\n ),\n );\n } else if (t.target !== entry) {\n issues.push(\n error(\n \"graph\",\n `step ${q(step.name)}: transition ${q(key)} re-purposes to ${q(t.purpose)} but targets ${q(t.target)}; it must target that purpose's entry step ${q(entry)}`,\n step.name,\n ),\n );\n }\n }\n }\n if (isCurrentFlow(t)) {\n if (!stepNames.has(t.target)) {\n issues.push(\n error(\n \"graph\",\n `step ${q(step.name)}: transition ${q(key)} targets unknown step ${q(t.target)}`,\n step.name,\n ),\n );\n }\n } else if (t.action !== \"switch\" && t.action !== \"pivot\") {\n issues.push(\n error(\n \"graph\",\n `step ${q(step.name)}: transition ${q(key)} has invalid action ${q(t.action ?? \"\")}`,\n step.name,\n ),\n );\n }\n }\n }\n\n for (const step of def.steps) {\n if (!step.terminal && step.transitions.size === 0) {\n issues.push(\n error(\"graph\", `step ${q(step.name)} is non-terminal but has no outgoing transitions`, step.name),\n );\n }\n }\n\n const reachable = new Set<string>();\n const adj = localAdjacency(def);\n for (const entryStep of def.purposes.values()) {\n bfs(entryStep, adj, reachable);\n }\n for (const step of def.steps) {\n if (!reachable.has(step.name)) {\n issues.push(error(\"graph\", `step ${q(step.name)} is unreachable from any entry point`, step.name));\n }\n }\n\n return issues;\n}\n\nfunction validateCycles(def: FlowDef): FlowValidationIssue[] {\n const issues: FlowValidationIssue[] = [];\n const reverse = reverseAdjacency(def);\n\n // Seed: escape nodes (terminal, or a switch/pivot out); BFS backwards.\n const safe = new Set<string>();\n const queue: string[] = [];\n for (const step of def.steps) {\n const escapes =\n step.terminal || [...step.transitions.values()].some((t) => !isCurrentFlow(t));\n if (escapes) {\n safe.add(step.name);\n queue.push(step.name);\n }\n }\n for (let head = 0; head < queue.length; head += 1) {\n const current = queue[head];\n if (current === undefined) continue;\n for (const pred of reverse.get(current) ?? []) {\n if (!safe.has(pred)) {\n safe.add(pred);\n queue.push(pred);\n }\n }\n }\n\n for (const step of def.steps) {\n if (!step.terminal && !safe.has(step.name)) {\n issues.push(\n error(\n \"cycles\",\n `step ${q(step.name)} is trapped: no path to a terminal step or another flow`,\n step.name,\n ),\n );\n }\n }\n return issues;\n}\n\nfunction validateFlipTableCoverage(def: FlowDef): FlowValidationIssue[] {\n const issues: FlowValidationIssue[] = [];\n for (const [purpose, entryStepName] of def.purposes) {\n const flipTargets = PURPOSE_FLIP_TARGETS[purpose];\n if (flipTargets === undefined) continue;\n const entry = def.steps.find((s) => s.name === entryStepName);\n if (entry === undefined) continue;\n for (const [outcome, targetPurpose] of Object.entries(flipTargets)) {\n if (!def.purposes.has(targetPurpose)) continue;\n if (!entry.transitions.has(outcome)) {\n issues.push(\n error(\n \"flip-table\",\n `step ${q(entry.name)}: entry step for purpose ${q(purpose)} must wire ${q(outcome)} transition because ${q(targetPurpose)} is also a purpose: without it, ${FLIP_OUTCOME_IMPACTS[outcome]}`,\n entry.name,\n ),\n );\n }\n }\n }\n return issues;\n}\n\n// ── phase 5: schema-dependent rules ──────────────────────────────────────────\n\nfunction schemaProperties(schema: Record<string, unknown>): Record<string, unknown> | undefined {\n return isPlainObject(schema.properties) ? schema.properties : undefined;\n}\n\nfunction schemaRequired(schema: Record<string, unknown>): string[] {\n return Array.isArray(schema.required) ? schema.required.map(str).filter((n) => n !== \"\") : [];\n}\n\n/**\n * Mirrors `schemaReader.RequiredPaths`: every name in `required`,\n * recursed through the nested `required` of each required object. A\n * required object declaring no `required` of its own ends the descent at\n * the object itself.\n *\n * An optional object contributes its own `required` too once it appears\n * in `materialized`. It only exists in the document because something\n * beneath it was collected, and from that point document validation\n * enforces the rest of its `required` list.\n */\nfunction requiredPaths(\n schema: Record<string, unknown>,\n materialized: ReadonlySet<string>,\n prefix = \"\",\n): string[] {\n const properties = schemaProperties(schema);\n const required = schemaRequired(schema);\n // A set, like the Go `map[string]struct{}`, so a name repeated in\n // `required` yields the path once.\n const out = new Set<string>();\n\n for (const name of required) {\n const path = prefix === \"\" ? name : `${prefix}.${name}`;\n const property =\n properties !== undefined && Object.hasOwn(properties, name) ? properties[name] : undefined;\n if (!isPlainObject(property) || schemaRequired(property).length === 0) {\n out.add(path);\n continue;\n }\n for (const nested of requiredPaths(property, materialized, path)) out.add(nested);\n }\n\n // An optional object is invisible to the loop above, so descend into\n // the ones a collected field materializes.\n for (const [name, property] of Object.entries(properties ?? {})) {\n if (required.includes(name) || !isPlainObject(property)) continue;\n const path = prefix === \"\" ? name : `${prefix}.${name}`;\n if (!materialized.has(path)) continue;\n for (const nested of requiredPaths(property, materialized, path)) out.add(nested);\n }\n\n return [...out];\n}\n\n/** Mirrors `xAuthMethodsReader.IsEnabled` in flow_field_resolver_schema.go. */\nfunction authMethodEnabled(schema: Record<string, unknown>, method: string): boolean {\n if (!isPlainObject(schema[\"x-auth-methods\"])) return false;\n const entry = schema[\"x-auth-methods\"][method];\n return isPlainObject(entry) && entry.enabled === true;\n}\n\n/**\n * Resolve one step field to its challenge, mirroring the subset of\n * `SchemaFieldResolver.Resolve` the validator needs. Returns either the\n * challenge (or null for none) or the Go-verbatim resolution error.\n */\nfunction resolveFieldChallenge(\n field: string,\n properties: Record<string, unknown>,\n schema: Record<string, unknown>,\n): { challenge: FieldChallenge | null } | { message: string } {\n if (field.startsWith(AUTH_METHOD_PREFIX)) {\n const method = field.slice(AUTH_METHOD_PREFIX.length);\n // Mirrors deriveAuthMethodType: only password is field-shaped today.\n if (method !== \"password\") {\n return { message: `unknown field type for ${q(field)}` };\n }\n if (!authMethodEnabled(schema, method)) {\n // Surfaced by resolveAllStepFields' enabled-method cross-check.\n return { message: `${q(method)} is not an enabled authentication method` };\n }\n return { challenge: \"password\" };\n }\n\n // Mirrors walkUserProperty: a nested property is addressed by its\n // dotted path, descending one `properties` level per segment.\n const segments = field.split(\".\");\n let property: unknown = undefined;\n let level: Record<string, unknown> | undefined = properties;\n for (const [i, segment] of segments.entries()) {\n // Own properties only: Go indexes a map, where an inherited name like\n // `toString` is simply absent.\n property = level !== undefined && Object.hasOwn(level, segment) ? level[segment] : undefined;\n if (property === undefined) {\n return { message: `flow field: not a property in the user schema: ${q(field)}` };\n }\n const nested = isPlainObject(property) ? schemaProperties(property) : undefined;\n if (i < segments.length - 1 && nested === undefined) {\n return { message: `flow field: not a property in the user schema: ${q(field)}` };\n }\n level = nested;\n }\n\n if (!isPlainObject(property)) {\n return { challenge: null };\n }\n\n // Mirrors schemaReader.JSONType: the nullable idiom `[\"null\", X]`\n // reduces to X; any other multi-entry union is unsupported. Go reduces\n // before it tests for a composite, so the union error wins and the\n // object/array test below reads the reduced type rather than the union.\n let jsonType = property.type;\n if (Array.isArray(jsonType)) {\n const nonNull = jsonType.filter((t) => t !== \"null\");\n if (nonNull.length > 1) {\n return { message: `flow field: unsupported JSON type: [${jsonType.map(str).join(\" \")}]` };\n }\n // Undefined when every entry was \"null\", matching Go's empty-string case.\n jsonType = nonNull[0];\n }\n\n // Mirrors deriveUserPropertyType: an object or array has no\n // field-shaped input. A property carrying `properties` is an object,\n // and one carrying `items` is an array, even when it omits the `type`\n // keyword.\n if (\n jsonType === \"object\" ||\n jsonType === \"array\" ||\n schemaProperties(property) !== undefined ||\n \"items\" in property\n ) {\n return {\n message: `flow field: not a scalar property, name a nested leaf instead: ${q(field)}`,\n };\n }\n\n // Mirrors deriveUnique + deriveIdentifierChallenge: any recognized\n // non-empty x-unique scope makes the property an identifier.\n const unique = property[\"x-unique\"];\n if (unique === \"project\" || unique === \"team\") {\n return { challenge: \"identifier\" };\n }\n return { challenge: null };\n}\n\nfunction validateAgainstSchema(def: FlowDef, schema: object): FlowValidationIssue[] {\n const issues: FlowValidationIssue[] = [];\n const raw = schema as Record<string, unknown>;\n\n const properties = schemaProperties(raw);\n if (properties === undefined) {\n issues.push(error(\"schema/fields-resolve\", \"user schema has no properties\"));\n return issues;\n }\n\n // Required-fields coverage (validateRequiredUserSchemaFields).\n // Collecting `address.street` covers the leaf and every object above\n // it, since an object materializes once one of its children is\n // collected. The same prefixes are what the schema treats as\n // materialized, so an optional object's own `required` list comes into\n // force here too.\n const covered = new Set<string>();\n for (const step of def.steps) {\n for (const field of step.fields) {\n let path = \"\";\n for (const node of field.split(\".\")) {\n path = path === \"\" ? node : `${path}.${node}`;\n covered.add(path);\n }\n }\n }\n const missing = requiredPaths(raw, covered)\n .filter((field) => !covered.has(field))\n .sort();\n if (missing.length > 0) {\n issues.push(\n error(\n \"schema/required-fields\",\n `required fields [${missing.join(\" \")}] in user schema are missing in the flow definition steps`,\n ),\n );\n }\n\n // Field resolution (resolveAllStepFields), collecting challenges for\n // the manifest and warning rules below.\n const challengesByStep = new Map<string, Set<FieldChallenge>>();\n let resolutionFailed = false;\n for (const step of def.steps) {\n const challenges = new Set<FieldChallenge>();\n for (const field of step.fields) {\n const resolved = resolveFieldChallenge(field, properties, raw);\n if (\"message\" in resolved) {\n issues.push(\n error(\"schema/fields-resolve\", `step ${q(step.name)}: ${resolved.message}`, step.name),\n );\n resolutionFailed = true;\n continue;\n }\n if (resolved.challenge !== null) {\n challenges.add(resolved.challenge);\n }\n }\n challengesByStep.set(step.name, challenges);\n }\n\n // Passkey enablement (validatePasskeyActionsEnabled): passkey is\n // action-shaped, not field-shaped, so the per-field enabled-method\n // check above never sees it.\n if (!authMethodEnabled(raw, \"passkey\")) {\n for (const step of def.steps) {\n for (const action of step.actions) {\n if (action.kind === \"passkey\" || action.kind === \"passkey_register\") {\n issues.push(\n error(\n \"schema/passkey-actions\",\n `step ${q(step.name)}: action ${q(action.name)} offers passkey but \"passkey\" is not an enabled authentication method`,\n step.name,\n ),\n );\n }\n }\n }\n }\n\n if (resolutionFailed) {\n return issues;\n }\n\n // on_success manifests (validateOnSuccessManifests): every kind the\n // mutation needs must be collected on the step or upstream.\n const reverse = reverseAdjacency(def);\n for (const step of def.steps) {\n if (step.onSuccess === undefined) continue;\n const manifest = ON_SUCCESS_MANIFESTS[step.onSuccess];\n if (manifest === undefined) continue;\n const upstream = new Set<string>();\n bfs(step.name, reverse, upstream);\n for (const kind of manifest) {\n const established = [...upstream].some((name) => challengesByStep.get(name)?.has(kind));\n if (!established) {\n issues.push(\n error(\n \"schema/on-success-manifest\",\n `step ${q(step.name)}: on_success ${step.onSuccess} requires ${q(kind)} to be collected upstream`,\n step.name,\n ),\n );\n }\n }\n }\n\n // CLI-only guidance (no Go counterpart): a password collected on the\n // login path with no identifier upstream leaves the engine unable to\n // resolve which user to challenge. Path-sensitive: walk forward from\n // the login entry and stop at any step that collects an identifier\n // (every route through it is covered), so an identifier on a\n // *different* route — e.g. only on the register-purpose chain into a\n // shared step — cannot mask a login route that never collects one.\n const loginEntry = def.purposes.get(\"login\");\n if (loginEntry !== undefined) {\n const adj = localAdjacency(def);\n const identifierFree = new Set<string>();\n const queue = [loginEntry];\n for (let head = 0; head < queue.length; head += 1) {\n const name = queue[head];\n if (name === undefined || identifierFree.has(name)) continue;\n if (challengesByStep.get(name)?.has(\"identifier\")) continue;\n identifierFree.add(name);\n queue.push(...(adj.get(name) ?? []));\n }\n for (const step of def.steps) {\n if (!identifierFree.has(step.name)) continue;\n if (!challengesByStep.get(step.name)?.has(\"password\")) continue;\n issues.push({\n severity: \"warning\",\n rule: \"warn/password-without-identifier\",\n message: `step ${q(step.name)} collects ${AUTH_METHOD_PREFIX}password on the login path but no upstream step collects an identifier field — the engine cannot resolve which user to challenge`,\n step: step.name,\n });\n }\n }\n\n return issues;\n}\n"],"mappings":"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"}
@@ -3,18 +3,13 @@
3
3
  "title": "AuthMethod",
4
4
  "type": "object",
5
5
  "required": [
6
- "enabled",
7
- "position"
6
+ "enabled"
8
7
  ],
8
+ "additionalProperties": false,
9
9
  "properties": {
10
10
  "enabled": {
11
11
  "type": "boolean",
12
12
  "description": "Whether the authentication method is enabled or not"
13
- },
14
- "position": {
15
- "type": "integer",
16
- "minimum": 0,
17
- "description": "The position of the authentication method in the list of supported methods"
18
13
  }
19
14
  }
20
15
  }
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Branding",
4
+ "description": "Local branding descriptor for the login UI. Carries the layout preset, asset URLs, and the LiquidJS template (by file reference or inline). `zitadel apply` publishes this as an immutable branding revision; flow responses resolve the latest revision per project (ADR 040). This schema describes the local configuration shape only.",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "$schema": {
9
+ "type": "string",
10
+ "description": "Editor affordance: path or URL of this meta-schema so editors validate and autocomplete the file (scaffolded branding files point at `.zitadel/meta/branding.json`). Ignored by the platform."
11
+ },
12
+ "layout": {
13
+ "type": "string",
14
+ "enum": ["centered", "split"],
15
+ "default": "centered",
16
+ "description": "Degrade preset (`centered` or `split`) the bundled default template branches on, not the complete design catalog. All named designs (`centered`, `split`, `split-right`, `hero`, `minimal`) ship as templates and map onto one of these two values; switch designs with the `branding eject --design <name>` command instead of editing this field. Custom templates may ignore it; the preset is also used when a template fails component-side validation."
17
+ },
18
+ "liquid_template_file": {
19
+ "type": "string",
20
+ "examples": ["./login.liquid"],
21
+ "description": "Path to the LiquidJS template file, relative to this descriptor. The CLI inlines the file content into `liquid_template` when publishing, and writes server state back to the file. Mutually exclusive with `liquid_template`."
22
+ },
23
+ "liquid_template": {
24
+ "type": "string",
25
+ "maxLength": 131072,
26
+ "description": "Inline LiquidJS template string. Prefer `liquid_template_file` — a real `.liquid` file edits far better than a JSON-escaped string. Mutually exclusive with `liquid_template_file`."
27
+ },
28
+ "logo_url": {
29
+ "type": "string",
30
+ "format": "uri",
31
+ "pattern": "^([hH][tT][tT][pP][sS]://|[hH][tT][tT][pP]://([lL][oO][cC][aA][lL][hH][oO][sS][tT]|127\\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|\\[::1\\])(?::0*(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?([/?#]|$))",
32
+ "description": "Logo URL (https required; canonical loopback http is allowed for local development and renders only on loopback pages)."
33
+ },
34
+ "hero_url": {
35
+ "type": "string",
36
+ "format": "uri",
37
+ "pattern": "^([hH][tT][tT][pP][sS]://|[hH][tT][tT][pP]://([lL][oO][cC][aA][lL][hH][oO][sS][tT]|127\\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|\\[::1\\])(?::0*(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?([/?#]|$))",
38
+ "description": "Hero/background image URL (https required; canonical loopback http is allowed for local development and renders only on loopback pages), used by split designs."
39
+ }
40
+ },
41
+ "not": {
42
+ "required": ["liquid_template", "liquid_template_file"]
43
+ }
44
+ }
@@ -330,13 +330,32 @@
330
330
  "description": "When `action` is omitted, the `name` of a step in the same definition. When `action` is `switch` or `pivot`, the `name` of another flow definition in the same project."
331
331
  },
332
332
  "action": {
333
- "type": ["string", "null"],
333
+ "type": [
334
+ "string",
335
+ "null"
336
+ ],
334
337
  "enum": [
335
338
  "switch",
336
339
  "pivot",
337
340
  null
338
341
  ],
339
342
  "description": "How to interpret `target`. - Omitted: transition to a step in the current definition. - `switch`: replace the current flow with the target flow. No return; the current flow is discarded. Typical use: navigating between peer flows like login ↔ register. - `pivot`: push the target flow onto the stack. The current flow is paused; when the target flow completes the engine auto-pops back. Typical use: login → recovery → back to login."
343
+ },
344
+ "purpose": {
345
+ "type": [
346
+ "string",
347
+ "null"
348
+ ],
349
+ "enum": [
350
+ "login",
351
+ "register",
352
+ "recovery",
353
+ "profiling",
354
+ "reauth",
355
+ "link_account",
356
+ null
357
+ ],
358
+ "description": "Local re-purposing. When set, taking this transition changes the flow's current purpose to this value — the dispatch mode a step's challenges run under — while the flow's original purpose stays pinned. Must be a purpose this definition serves, and `target` must be that purpose's entry step. Mutually exclusive with `action`: a transition either re-purposes within this flow or targets another flow, never both. Example: a \"Sign up\" navigation on the login identifier step (`{ \"target\": \"register\", \"purpose\": \"register\" }`)."
340
359
  }
341
360
  }
342
361
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "PropertyName",
4
+ "type": "string",
5
+ "pattern": "^[^.]+$",
6
+ "description": "The name of a single user attribute."
7
+ }