@zitadel/config 0.1.0-alpha.17 → 0.1.0-alpha.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/defaults/README-branding.md +62 -0
- package/defaults/branding/centered/login.liquid +164 -0
- package/defaults/branding/hero/login.liquid +222 -0
- package/defaults/branding/minimal/login.liquid +154 -0
- package/defaults/branding/split/login.liquid +178 -0
- package/defaults/branding/split-right/login.liquid +178 -0
- package/defaults/default-human-user.json +0 -15
- package/defaults/default-login.json +1 -4
- package/defaults/presets/passkey-first/human-user.json +0 -15
- package/defaults/presets/passkey-first/login.json +1 -4
- package/dist/defaults-Bh4_xVbh.d.mts +84 -0
- package/dist/defaults-Bh4_xVbh.d.mts.map +1 -0
- package/dist/{defaults-BQej3KIY.mjs → defaults-CXTnFMHj.mjs} +193 -65
- package/dist/defaults-CXTnFMHj.mjs.map +1 -0
- package/dist/defaults.d.mts +2 -2
- package/dist/defaults.mjs +2 -2
- package/dist/index.d.mts +5 -4
- package/dist/index.mjs +5 -4
- package/dist/{meta-schemas-8nlyVL7g.mjs → meta-schemas-B4YiHAX_.mjs} +71 -3
- package/dist/meta-schemas-B4YiHAX_.mjs.map +1 -0
- package/dist/meta-schemas.d.mts +6 -1
- package/dist/meta-schemas.d.mts.map +1 -1
- package/dist/meta-schemas.mjs +2 -2
- package/dist/schemas.d.mts +14 -1
- package/dist/schemas.d.mts.map +1 -1
- package/dist/schemas.mjs +60 -2
- package/dist/schemas.mjs.map +1 -1
- package/dist/template.d.mts +76 -0
- package/dist/template.d.mts.map +1 -0
- package/dist/template.mjs +145 -0
- package/dist/template.mjs.map +1 -0
- package/meta-schemas/branding.json +52 -0
- package/meta-schemas/property-name.json +7 -0
- package/meta-schemas/user-property.json +6 -1
- package/meta-schemas/user-schema.json +4 -1
- package/package.json +12 -4
- package/dist/defaults-BQej3KIY.mjs.map +0 -1
- package/dist/defaults-CSjBjBkQ.d.mts +0 -41
- package/dist/defaults-CSjBjBkQ.d.mts.map +0 -1
- package/dist/meta-schemas-8nlyVL7g.mjs.map +0 -1
package/dist/schemas.mjs
CHANGED
|
@@ -1,9 +1,67 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CreateBrandingBody, CreateFlowDefinitionBody, CreateSchemaBody } from "@zitadel/api/generated/endpoints/zitadelNextGen.zod";
|
|
2
3
|
//#region src/schemas.ts
|
|
3
4
|
const schemaConfigSchema = CreateSchemaBody;
|
|
4
5
|
const flowConfigSchema = CreateFlowDefinitionBody.shape.flow_definition;
|
|
5
6
|
const createFlowDefinitionRequestSchema = CreateFlowDefinitionBody;
|
|
7
|
+
/** The wire shape of a branding revision (`POST /branding` request body). */
|
|
8
|
+
const brandingWireSchema = CreateBrandingBody;
|
|
9
|
+
/**
|
|
10
|
+
* The local `.zitadel/branding/*.json` dialect: the wire shape plus
|
|
11
|
+
* `liquid_template_file`, a descriptor-relative path the CLI inlines into
|
|
12
|
+
* `liquid_template` when publishing. Exactly one of the two template carriers
|
|
13
|
+
* may be present (the meta-schema `branding.json` mirrors this).
|
|
14
|
+
*/
|
|
15
|
+
const brandingConfigSchema = z.strictObject({
|
|
16
|
+
...CreateBrandingBody.shape,
|
|
17
|
+
logo_url: z.string().optional(),
|
|
18
|
+
hero_url: z.string().optional(),
|
|
19
|
+
$schema: z.string().optional(),
|
|
20
|
+
liquid_template_file: z.string().min(1).optional()
|
|
21
|
+
}).superRefine((value, ctx) => {
|
|
22
|
+
if (value.liquid_template !== void 0 && value.liquid_template_file !== void 0) ctx.addIssue({
|
|
23
|
+
code: "custom",
|
|
24
|
+
message: "Use either liquid_template_file or an inline liquid_template, not both."
|
|
25
|
+
});
|
|
26
|
+
if (value.font_url !== void 0) ctx.addIssue({
|
|
27
|
+
code: "custom",
|
|
28
|
+
message: "font_url is not writable yet (tenant font delivery needs a safe design, see ADR 040); load fonts from the embedding page instead."
|
|
29
|
+
});
|
|
30
|
+
requireHttpsUrl(value.logo_url, "logo_url", ctx);
|
|
31
|
+
requireHttpsUrl(value.hero_url, "hero_url", ctx);
|
|
32
|
+
});
|
|
33
|
+
function requireHttpsUrl(value, field, ctx) {
|
|
34
|
+
if (value === void 0 || value === "") return;
|
|
35
|
+
if (/[\s\\]/.test(value)) {
|
|
36
|
+
ctx.addIssue({
|
|
37
|
+
code: "custom",
|
|
38
|
+
message: `${field} is not a valid URL.`
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!/^https:\/\//i.test(value)) {
|
|
43
|
+
ctx.addIssue({
|
|
44
|
+
code: "custom",
|
|
45
|
+
message: `${field} must be an absolute https URL.`
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = new URL(value);
|
|
52
|
+
} catch {
|
|
53
|
+
ctx.addIssue({
|
|
54
|
+
code: "custom",
|
|
55
|
+
message: `${field} is not a valid URL.`
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (parsed.protocol !== "https:" || parsed.host === "") ctx.addIssue({
|
|
60
|
+
code: "custom",
|
|
61
|
+
message: `${field} must be an absolute https URL.`
|
|
62
|
+
});
|
|
63
|
+
}
|
|
6
64
|
//#endregion
|
|
7
|
-
export { createFlowDefinitionRequestSchema, flowConfigSchema, schemaConfigSchema };
|
|
65
|
+
export { brandingConfigSchema, brandingWireSchema, createFlowDefinitionRequestSchema, flowConfigSchema, schemaConfigSchema };
|
|
8
66
|
|
|
9
67
|
//# sourceMappingURL=schemas.mjs.map
|
package/dist/schemas.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemas.mjs","names":[],"sources":["../src/schemas.ts"],"sourcesContent":["import {\n CreateFlowDefinitionBody,\n CreateSchemaBody,\n} from \"@zitadel/api/generated/endpoints/zitadelNextGen.zod\";\n\nexport const schemaConfigSchema = CreateSchemaBody;\nexport const flowConfigSchema = CreateFlowDefinitionBody.shape.flow_definition;\nexport const createFlowDefinitionRequestSchema = CreateFlowDefinitionBody;\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"schemas.mjs","names":[],"sources":["../src/schemas.ts"],"sourcesContent":["import { z } from \"zod\";\n\nimport {\n CreateBrandingBody,\n CreateFlowDefinitionBody,\n CreateSchemaBody,\n} from \"@zitadel/api/generated/endpoints/zitadelNextGen.zod\";\n\nexport const schemaConfigSchema = CreateSchemaBody;\nexport const flowConfigSchema = CreateFlowDefinitionBody.shape.flow_definition;\nexport const createFlowDefinitionRequestSchema = CreateFlowDefinitionBody;\n\n/** The wire shape of a branding revision (`POST /branding` request body). */\nexport const brandingWireSchema = CreateBrandingBody;\n\n/**\n * The local `.zitadel/branding/*.json` dialect: the wire shape plus\n * `liquid_template_file`, a descriptor-relative path the CLI inlines into\n * `liquid_template` when publishing. Exactly one of the two template carriers\n * may be present (the meta-schema `branding.json` mirrors this).\n */\nexport const brandingConfigSchema = z\n .strictObject({\n // Strict, unlike the generated wire schema: a typo like `hero_urll`\n // must fail plan, not silently pass, get ignored by the server, and\n // vanish on canonical write-back. The editor meta-schema already\n // rejects unknown keys; the CLI gate has to agree.\n ...CreateBrandingBody.shape,\n // Raw strings instead of the generated uri fields: those trim and\n // normalize before refinement runs, hiding exactly the forms the wire\n // body still carries verbatim and Go's url.Parse rejects.\n logo_url: z.string().optional(),\n hero_url: z.string().optional(),\n $schema: z.string().optional(),\n liquid_template_file: z.string().min(1).optional(),\n })\n .superRefine((value, ctx) => {\n if (value.liquid_template !== undefined && value.liquid_template_file !== undefined) {\n ctx.addIssue({\n code: \"custom\",\n message: \"Use either liquid_template_file or an inline liquid_template, not both.\",\n });\n }\n // font_url is read-only in v1: the login component loads it as a\n // document-level stylesheet (shadow-scoped @font-face never registers\n // faces), which would give branding.write arbitrary CSS over the embedding\n // page. The server rejects it too; safe delivery is an ADR 040 follow-up.\n if (value.font_url !== undefined) {\n ctx.addIssue({\n code: \"custom\",\n message:\n \"font_url is not writable yet (tenant font delivery needs a safe design, see ADR 040); load fonts from the embedding page instead.\",\n });\n }\n // Mirror the server's https-only asset gate (validateBrandingAssetURL in\n // internal/domain/branding_validator.go) so plan rejects what apply would —\n // apply mutates schemas and flows before branding, so a late 400 here\n // would leave a half-applied run.\n requireHttpsUrl(value.logo_url, \"logo_url\", ctx);\n requireHttpsUrl(value.hero_url, \"hero_url\", ctx);\n});\n\nfunction requireHttpsUrl(\n value: string | undefined,\n field: \"logo_url\" | \"hero_url\",\n ctx: z.RefinementCtx,\n): void {\n if (value === undefined || value === \"\") {\n return;\n }\n // Stricter-or-equal than the Go gate (validateBrandingAssetURL): the WHATWG\n // parser forgives what Go's url.Parse rejects — `https:example.com`,\n // backslashes, and whitespace all get silently normalized. plan must never\n // accept what apply would reject (the reverse is harmless), so reject the\n // raw forms before parsing.\n if (/[\\s\\\\]/.test(value)) {\n ctx.addIssue({ code: \"custom\", message: `${field} is not a valid URL.` });\n return;\n }\n if (!/^https:\\/\\//i.test(value)) {\n ctx.addIssue({ code: \"custom\", message: `${field} must be an absolute https URL.` });\n return;\n }\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n ctx.addIssue({ code: \"custom\", message: `${field} is not a valid URL.` });\n return;\n }\n if (parsed.protocol !== \"https:\" || parsed.host === \"\") {\n ctx.addIssue({ code: \"custom\", message: `${field} must be an absolute https URL.` });\n }\n}\n"],"mappings":";;;AAQA,MAAa,qBAAqB;AAClC,MAAa,mBAAmB,yBAAyB,MAAM;AAC/D,MAAa,oCAAoC;;AAGjD,MAAa,qBAAqB;;;;;;;AAQlC,MAAa,uBAAuB,EACjC,aAAa;CAKZ,GAAG,mBAAmB;CAItB,UAAU,EAAE,QAAQ,CAAC,UAAU;CAC/B,UAAU,EAAE,QAAQ,CAAC,UAAU;CAC/B,SAAS,EAAE,QAAQ,CAAC,UAAU;CAC9B,sBAAsB,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,UAAU;CACnD,CAAC,CACD,aAAa,OAAO,QAAQ;AAC7B,KAAI,MAAM,oBAAoB,KAAA,KAAa,MAAM,yBAAyB,KAAA,EACxE,KAAI,SAAS;EACX,MAAM;EACN,SAAS;EACV,CAAC;AAMJ,KAAI,MAAM,aAAa,KAAA,EACrB,KAAI,SAAS;EACX,MAAM;EACN,SACE;EACH,CAAC;AAMJ,iBAAgB,MAAM,UAAU,YAAY,IAAI;AAChD,iBAAgB,MAAM,UAAU,YAAY,IAAI;EAChD;AAEF,SAAS,gBACP,OACA,OACA,KACM;AACN,KAAI,UAAU,KAAA,KAAa,UAAU,GACnC;AAOF,KAAI,SAAS,KAAK,MAAM,EAAE;AACxB,MAAI,SAAS;GAAE,MAAM;GAAU,SAAS,GAAG,MAAM;GAAuB,CAAC;AACzE;;AAEF,KAAI,CAAC,eAAe,KAAK,MAAM,EAAE;AAC/B,MAAI,SAAS;GAAE,MAAM;GAAU,SAAS,GAAG,MAAM;GAAkC,CAAC;AACpF;;CAEF,IAAI;AACJ,KAAI;AACF,WAAS,IAAI,IAAI,MAAM;SACjB;AACN,MAAI,SAAS;GAAE,MAAM;GAAU,SAAS,GAAG,MAAM;GAAuB,CAAC;AACzE;;AAEF,KAAI,OAAO,aAAa,YAAY,OAAO,SAAS,GAClD,KAAI,SAAS;EAAE,MAAM;EAAU,SAAS,GAAG,MAAM;EAAkC,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//#region src/template.d.ts
|
|
2
|
+
/** The `{% mandatory_gates %}` runtime safety-net tag every template must emit. */
|
|
3
|
+
declare const MANDATORY_GATES_TAG = "mandatory_gates";
|
|
4
|
+
/**
|
|
5
|
+
* Custom Liquid tags the login orchestrator registers beyond the standard
|
|
6
|
+
* set. Validators must register them (as no-ops) or parsing valid templates
|
|
7
|
+
* fails; `@zitadel/components` registers the real implementations.
|
|
8
|
+
*/
|
|
9
|
+
declare const TEMPLATE_TAGS: readonly ["mandatory_gates"];
|
|
10
|
+
/**
|
|
11
|
+
* Byte cap for stored templates. Mirrors `maxLength` on
|
|
12
|
+
* `branding.liquid_template` in the OpenAPI contract and
|
|
13
|
+
* `MaxBrandingTemplateBytes` in the Go gate.
|
|
14
|
+
*/
|
|
15
|
+
declare const MAX_TEMPLATE_BYTES = 131072;
|
|
16
|
+
type TemplateValidationSeverity = "error" | "warning";
|
|
17
|
+
/**
|
|
18
|
+
* Rule catalog with pointers into the Go lexical gate
|
|
19
|
+
* (`internal/domain/branding_validator.go`) where a server-side mirror
|
|
20
|
+
* exists. `parse` and `mandatory-gates` are authoring-only: the server
|
|
21
|
+
* cannot check them (wrong dialect) and render-time behavior degrades
|
|
22
|
+
* safely without them.
|
|
23
|
+
*/
|
|
24
|
+
declare const TEMPLATE_VALIDATION_RULES: {
|
|
25
|
+
readonly parse: {
|
|
26
|
+
readonly goRef: null;
|
|
27
|
+
};
|
|
28
|
+
readonly size: {
|
|
29
|
+
readonly goRef: "MaxBrandingTemplateBytes";
|
|
30
|
+
};
|
|
31
|
+
readonly "no-script-tag": {
|
|
32
|
+
readonly goRef: "brandingScriptTag";
|
|
33
|
+
};
|
|
34
|
+
readonly "no-style-tag": {
|
|
35
|
+
readonly goRef: "brandingStyleTag";
|
|
36
|
+
};
|
|
37
|
+
readonly "no-inline-handlers": {
|
|
38
|
+
readonly goRef: "brandingInlineHandler";
|
|
39
|
+
};
|
|
40
|
+
readonly "no-javascript-urls": {
|
|
41
|
+
readonly goRef: "brandingJavascriptURL";
|
|
42
|
+
};
|
|
43
|
+
readonly "no-raw-filter": {
|
|
44
|
+
readonly goRef: "brandingRawFilter";
|
|
45
|
+
};
|
|
46
|
+
readonly "mandatory-gates": {
|
|
47
|
+
readonly goRef: null;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
type TemplateValidationRuleId = keyof typeof TEMPLATE_VALIDATION_RULES;
|
|
51
|
+
type TemplateValidationIssue = {
|
|
52
|
+
severity: TemplateValidationSeverity;
|
|
53
|
+
rule: TemplateValidationRuleId;
|
|
54
|
+
message: string;
|
|
55
|
+
};
|
|
56
|
+
type BannedPattern = {
|
|
57
|
+
rule: TemplateValidationRuleId;
|
|
58
|
+
pattern: RegExp;
|
|
59
|
+
message: string;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Lexical bans, identical to the Go gate. The inline-handler pattern only
|
|
63
|
+
* matches inside an open tag so Liquid like `{% assign online = true %}`
|
|
64
|
+
* doesn't false-positive; its separator class includes `/` because HTML
|
|
65
|
+
* treats a slash between attributes as whitespace (`<img/onerror=…>`).
|
|
66
|
+
*/
|
|
67
|
+
declare const BANNED_TEMPLATE_PATTERNS: readonly BannedPattern[];
|
|
68
|
+
/**
|
|
69
|
+
* Validates a login template. Pure and non-throwing, like
|
|
70
|
+
* `validateFlowDefinition`: returns an issue list, empty when the template
|
|
71
|
+
* is valid.
|
|
72
|
+
*/
|
|
73
|
+
declare function validateLoginTemplate(template: string): TemplateValidationIssue[];
|
|
74
|
+
//#endregion
|
|
75
|
+
export { BANNED_TEMPLATE_PATTERNS, MANDATORY_GATES_TAG, MAX_TEMPLATE_BYTES, TEMPLATE_TAGS, TEMPLATE_VALIDATION_RULES, TemplateValidationIssue, TemplateValidationRuleId, TemplateValidationSeverity, validateLoginTemplate };
|
|
76
|
+
//# sourceMappingURL=template.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.d.mts","names":[],"sources":["../src/template.ts"],"mappings":";;cAkBa,mBAAA;;;;;AAOb;cAAa,aAAA;;;;AAOb;;cAAa,kBAAA;AAAA,KAED,0BAAA;;AAAZ;;;;;AASA;cAAa,yBAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;KAWD,wBAAA,gBAAwC,yBAAA;AAAA,KAExC,uBAAA;EACV,QAAA,EAAU,0BAAA;EACV,IAAA,EAAM,wBAAA;EACN,OAAA;AAAA;AAAA,KAGG,aAAA;EACH,IAAA,EAAM,wBAAA;EACN,OAAA,EAAS,MAAA;EACT,OAAA;AAAA;AALA;;;;;;AAAA,cAcW,wBAAA,WAAmC,aAAA;;;;;AAAhD;iBAqEgB,qBAAA,CAAsB,QAAA,WAAmB,uBAAA"}
|
|
@@ -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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
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": [
|
|
15
|
+
"centered",
|
|
16
|
+
"split"
|
|
17
|
+
],
|
|
18
|
+
"default": "centered",
|
|
19
|
+
"description": "Layout preset the bundled default template branches on. Custom templates may ignore it; it also serves as the degrade target when a template fails component-side validation."
|
|
20
|
+
},
|
|
21
|
+
"liquid_template_file": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"examples": [
|
|
24
|
+
"./login.liquid"
|
|
25
|
+
],
|
|
26
|
+
"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`."
|
|
27
|
+
},
|
|
28
|
+
"liquid_template": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"maxLength": 131072,
|
|
31
|
+
"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`."
|
|
32
|
+
},
|
|
33
|
+
"logo_url": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"format": "uri",
|
|
36
|
+
"pattern": "^[hH][tT][tT][pP][sS]://",
|
|
37
|
+
"description": "Logo URL (https required)."
|
|
38
|
+
},
|
|
39
|
+
"hero_url": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"format": "uri",
|
|
42
|
+
"pattern": "^[hH][tT][tT][pP][sS]://",
|
|
43
|
+
"description": "Hero/background image URL (https required), used by split designs."
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"not": {
|
|
47
|
+
"required": [
|
|
48
|
+
"liquid_template",
|
|
49
|
+
"liquid_template_file"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -70,7 +70,12 @@
|
|
|
70
70
|
"properties": {
|
|
71
71
|
"type": "object",
|
|
72
72
|
"description": "A map of additional properties for the user definition, where the key is the property name and the value is the property schema",
|
|
73
|
-
"
|
|
73
|
+
"propertyNames": {
|
|
74
|
+
"$ref": "property-name.json"
|
|
75
|
+
},
|
|
76
|
+
"additionalProperties": {
|
|
77
|
+
"$ref": "user-property.json"
|
|
78
|
+
}
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
81
|
}
|
|
@@ -42,9 +42,12 @@
|
|
|
42
42
|
"properties": {
|
|
43
43
|
"type": "object",
|
|
44
44
|
"description": "A map of additional properties for the user definition, where the key is the property name and the value is the property schema",
|
|
45
|
+
"propertyNames": {
|
|
46
|
+
"$ref": "property-name.json"
|
|
47
|
+
},
|
|
45
48
|
"additionalProperties": {
|
|
46
49
|
"$ref": "user-property.json"
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
|
-
}
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zitadel/config",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.18",
|
|
4
4
|
"description": "Versioned Zitadel local config schemas and defaults",
|
|
5
5
|
"homepage": "https://github.com/zitadel/nextgen/tree/main/packages/config#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"defaults/*.json",
|
|
18
18
|
"defaults/*.md",
|
|
19
19
|
"defaults/presets/*/*.json",
|
|
20
|
+
"defaults/branding/*/*.liquid",
|
|
20
21
|
"meta-schemas/*.json",
|
|
21
22
|
"LICENSE"
|
|
22
23
|
],
|
|
@@ -52,6 +53,11 @@
|
|
|
52
53
|
"types": "./dist/meta-schemas.d.mts",
|
|
53
54
|
"import": "./dist/meta-schemas.mjs"
|
|
54
55
|
},
|
|
56
|
+
"./template": {
|
|
57
|
+
"@zitadel/source": "./src/template.ts",
|
|
58
|
+
"types": "./dist/template.d.mts",
|
|
59
|
+
"import": "./dist/template.mjs"
|
|
60
|
+
},
|
|
55
61
|
"./defaults/default-human-user.json": "./defaults/default-human-user.json",
|
|
56
62
|
"./defaults/default-login.json": "./defaults/default-login.json",
|
|
57
63
|
"./package.json": "./package.json"
|
|
@@ -60,17 +66,19 @@
|
|
|
60
66
|
"access": "public"
|
|
61
67
|
},
|
|
62
68
|
"dependencies": {
|
|
63
|
-
"
|
|
69
|
+
"liquidjs": "^10.27.2",
|
|
70
|
+
"zod": "^4.3.6",
|
|
71
|
+
"@zitadel/api": "0.1.0-alpha.18"
|
|
64
72
|
},
|
|
65
73
|
"devDependencies": {
|
|
66
74
|
"ajv": "^8.20.0",
|
|
67
75
|
"tsdown": "^0.21.10",
|
|
68
76
|
"tslib": "^2.8.1",
|
|
69
|
-
"vitest": "^4.1.
|
|
77
|
+
"vitest": "^4.1.9"
|
|
70
78
|
},
|
|
71
79
|
"scripts": {
|
|
72
80
|
"build": "tsdown",
|
|
73
|
-
"typecheck": "tsc --
|
|
81
|
+
"typecheck": "tsc --build tsconfig.json",
|
|
74
82
|
"test": "vitest run --passWithNoTests",
|
|
75
83
|
"lint": "oxlint ."
|
|
76
84
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"defaults-BQej3KIY.mjs","names":["defaultHumanUserSchemaTemplate","defaultLoginFlowTemplate","passkeyFirstHumanUserSchemaTemplate","passkeyFirstLoginFlowTemplate"],"sources":["../defaults/default-human-user.json","../defaults/default-login.json","../defaults/presets/passkey-first/human-user.json","../defaults/presets/passkey-first/login.json","../src/readmes.ts","../src/defaults.ts"],"sourcesContent":["","","","","import { readFileSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * README content the CLI copies to `.zitadel/schemas/README.md` during setup.\n * Source of truth is `packages/config/defaults/README-schemas.md`; the Go\n * server-side embed reads the same file (see `defaults.go`).\n */\nexport function schemasReadmeContent(): string {\n return readFileSync(readmeUrl(\"README-schemas.md\"), \"utf8\");\n}\n\n/**\n * README content the CLI copies to `.zitadel/flows/README.md` during setup.\n * Source of truth is `packages/config/defaults/README-flows.md`.\n */\nexport function flowsReadmeContent(): string {\n return readFileSync(readmeUrl(\"README-flows.md\"), \"utf8\");\n}\n\nfunction readmeUrl(filename: string): string {\n return fileURLToPath(new URL(`../defaults/${filename}`, import.meta.url));\n}\n","import type {\n CreateFlowDefinitionBodyFlowDefinition,\n CreateSchemaBody,\n} from \"@zitadel/api/generated/model\";\n\nimport defaultHumanUserSchemaTemplate from \"../defaults/default-human-user.json\" with {\n type: \"json\",\n};\nimport defaultLoginFlowTemplate from \"../defaults/default-login.json\" with {\n type: \"json\",\n};\nimport passkeyFirstHumanUserSchemaTemplate from \"../defaults/presets/passkey-first/human-user.json\" with {\n type: \"json\",\n};\nimport passkeyFirstLoginFlowTemplate from \"../defaults/presets/passkey-first/login.json\" with {\n type: \"json\",\n};\n\nexport { flowsReadmeContent, schemasReadmeContent } from \"./readmes.js\";\n\nexport const DEFAULT_BUILTIN_SCHEMA_BASE = \"https://nextgen.com/api/schemas\";\nexport const DEFAULT_FLOW_SCHEMA_URI = \"https://nextgen.com/flow-definition.json\";\nexport const DEFAULT_SCHEMA_CONFIG_PATH = \".zitadel/schemas/default-human-user.json\";\nexport const DEFAULT_FLOW_CONFIG_PATH = \".zitadel/flows/default-login.json\";\n\n/**\n * Named schema+flow bundles `zitadel setup` can scaffold (#448: the prompt\n * fires before any `.zitadel/` file is written; each preset maps to a\n * pre-defined bundle the CLI copies on first setup). `password-first` is\n * today's default; `passkey-first` puts a passkey ceremony on the login\n * entry step with an email→password fallback path.\n */\nexport const SETUP_PRESETS = [\"password-first\", \"passkey-first\"] as const;\n\nexport type SetupPreset = (typeof SETUP_PRESETS)[number];\n\nexport const DEFAULT_SETUP_PRESET: SetupPreset = \"password-first\";\n\nconst PRESET_TEMPLATES: Record<SetupPreset, { schema: unknown; flow: unknown }> = {\n \"password-first\": {\n schema: defaultHumanUserSchemaTemplate,\n flow: defaultLoginFlowTemplate,\n },\n \"passkey-first\": {\n schema: passkeyFirstHumanUserSchemaTemplate,\n flow: passkeyFirstLoginFlowTemplate,\n },\n};\n\nfunction presetTemplates(preset: string): { schema: unknown; flow: unknown } {\n // Own-property check: indexing a plain object with an arbitrary string\n // would let \"__proto__\"/\"constructor\" resolve via the prototype chain\n // and bypass the unknown-preset error.\n if (!Object.hasOwn(PRESET_TEMPLATES, preset)) {\n throw new Error(\n `unknown setup preset ${JSON.stringify(preset)} (known presets: ${SETUP_PRESETS.join(\", \")})`,\n );\n }\n return PRESET_TEMPLATES[preset as SetupPreset];\n}\n\nexport type DefaultConfigRenderOptions = {\n builtinSchemaBase?: string;\n userSchemaUrl?: string;\n /** Which bundle to render; defaults to {@link DEFAULT_SETUP_PRESET}. */\n preset?: string;\n};\n\nexport function defaultHumanUserSchemaUrl(\n builtinSchemaBase = DEFAULT_BUILTIN_SCHEMA_BASE,\n): string {\n return `${trimTrailingSlash(builtinSchemaBase)}/default-human-user.json`;\n}\n\nexport function getDefaultHumanUserSchema(\n options: DefaultConfigRenderOptions = {},\n): CreateSchemaBody {\n const builtinSchemaBase = trimTrailingSlash(\n options.builtinSchemaBase ?? DEFAULT_BUILTIN_SCHEMA_BASE,\n );\n return renderTemplate(presetTemplates(options.preset ?? DEFAULT_SETUP_PRESET).schema, {\n SERVER_URL: builtinSchemaBase,\n USER_SCHEMA_URL: options.userSchemaUrl ?? defaultHumanUserSchemaUrl(builtinSchemaBase),\n }) as CreateSchemaBody;\n}\n\nexport function getDefaultLoginFlow(\n options: DefaultConfigRenderOptions = {},\n): CreateFlowDefinitionBodyFlowDefinition {\n const builtinSchemaBase = trimTrailingSlash(\n options.builtinSchemaBase ?? DEFAULT_BUILTIN_SCHEMA_BASE,\n );\n return renderTemplate(presetTemplates(options.preset ?? DEFAULT_SETUP_PRESET).flow, {\n SERVER_URL: builtinSchemaBase,\n USER_SCHEMA_URL: options.userSchemaUrl ?? defaultHumanUserSchemaUrl(builtinSchemaBase),\n }) as CreateFlowDefinitionBodyFlowDefinition;\n}\n\nfunction renderTemplate(value: unknown, replacements: Record<string, string>): unknown {\n if (Array.isArray(value)) {\n return value.map((item) => renderTemplate(item, replacements));\n }\n if (value !== null && typeof value === \"object\") {\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [key, renderTemplate(item, replacements)]),\n );\n }\n if (typeof value === \"string\") {\n return value.replaceAll(/\\$\\{([A-Z_]+)\\}/g, (match, name: string) => {\n return replacements[name] ?? match;\n });\n }\n return value;\n}\n\nfunction trimTrailingSlash(value: string): string {\n return value.replace(/\\/+$/u, \"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AIQA,SAAgB,uBAA+B;AAC7C,QAAO,aAAa,UAAU,oBAAoB,EAAE,OAAO;;;;;;AAO7D,SAAgB,qBAA6B;AAC3C,QAAO,aAAa,UAAU,kBAAkB,EAAE,OAAO;;AAG3D,SAAS,UAAU,UAA0B;AAC3C,QAAO,cAAc,IAAI,IAAI,eAAe,YAAY,OAAO,KAAK,IAAI,CAAC;;;;ACD3E,MAAa,8BAA8B;AAC3C,MAAa,0BAA0B;AACvC,MAAa,6BAA6B;AAC1C,MAAa,2BAA2B;;;;;;;;AASxC,MAAa,gBAAgB,CAAC,kBAAkB,gBAAgB;AAIhE,MAAa,uBAAoC;AAEjD,MAAM,mBAA4E;CAChF,kBAAkB;EAChB,QAAQA;EACR,MAAMC;EACP;CACD,iBAAiB;EACf,QAAQC;EACR,MAAMC;EACP;CACF;AAED,SAAS,gBAAgB,QAAoD;AAI3E,KAAI,CAAC,OAAO,OAAO,kBAAkB,OAAO,CAC1C,OAAM,IAAI,MACR,wBAAwB,KAAK,UAAU,OAAO,CAAC,mBAAmB,cAAc,KAAK,KAAK,CAAC,GAC5F;AAEH,QAAO,iBAAiB;;AAU1B,SAAgB,0BACd,oBAAoB,6BACZ;AACR,QAAO,GAAG,kBAAkB,kBAAkB,CAAC;;AAGjD,SAAgB,0BACd,UAAsC,EAAE,EACtB;CAClB,MAAM,oBAAoB,kBACxB,QAAQ,qBAAA,kCACT;AACD,QAAO,eAAe,gBAAgB,QAAQ,UAAA,iBAA+B,CAAC,QAAQ;EACpF,YAAY;EACZ,iBAAiB,QAAQ,iBAAiB,0BAA0B,kBAAkB;EACvF,CAAC;;AAGJ,SAAgB,oBACd,UAAsC,EAAE,EACA;CACxC,MAAM,oBAAoB,kBACxB,QAAQ,qBAAA,kCACT;AACD,QAAO,eAAe,gBAAgB,QAAQ,UAAA,iBAA+B,CAAC,MAAM;EAClF,YAAY;EACZ,iBAAiB,QAAQ,iBAAiB,0BAA0B,kBAAkB;EACvF,CAAC;;AAGJ,SAAS,eAAe,OAAgB,cAA+C;AACrF,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,KAAK,SAAS,eAAe,MAAM,aAAa,CAAC;AAEhE,KAAI,UAAU,QAAQ,OAAO,UAAU,SACrC,QAAO,OAAO,YACZ,OAAO,QAAQ,MAAM,CAAC,KAAK,CAAC,KAAK,UAAU,CAAC,KAAK,eAAe,MAAM,aAAa,CAAC,CAAC,CACtF;AAEH,KAAI,OAAO,UAAU,SACnB,QAAO,MAAM,WAAW,qBAAqB,OAAO,SAAiB;AACnE,SAAO,aAAa,SAAS;GAC7B;AAEJ,QAAO;;AAGT,SAAS,kBAAkB,OAAuB;AAChD,QAAO,MAAM,QAAQ,SAAS,GAAG"}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { CreateFlowDefinitionBodyFlowDefinition, CreateSchemaBody } from "@zitadel/api/generated/model";
|
|
2
|
-
|
|
3
|
-
//#region src/readmes.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* README content the CLI copies to `.zitadel/schemas/README.md` during setup.
|
|
6
|
-
* Source of truth is `packages/config/defaults/README-schemas.md`; the Go
|
|
7
|
-
* server-side embed reads the same file (see `defaults.go`).
|
|
8
|
-
*/
|
|
9
|
-
declare function schemasReadmeContent(): string;
|
|
10
|
-
/**
|
|
11
|
-
* README content the CLI copies to `.zitadel/flows/README.md` during setup.
|
|
12
|
-
* Source of truth is `packages/config/defaults/README-flows.md`.
|
|
13
|
-
*/
|
|
14
|
-
declare function flowsReadmeContent(): string;
|
|
15
|
-
//#endregion
|
|
16
|
-
//#region src/defaults.d.ts
|
|
17
|
-
declare const DEFAULT_BUILTIN_SCHEMA_BASE = "https://nextgen.com/api/schemas";
|
|
18
|
-
declare const DEFAULT_FLOW_SCHEMA_URI = "https://nextgen.com/flow-definition.json";
|
|
19
|
-
declare const DEFAULT_SCHEMA_CONFIG_PATH = ".zitadel/schemas/default-human-user.json";
|
|
20
|
-
declare const DEFAULT_FLOW_CONFIG_PATH = ".zitadel/flows/default-login.json";
|
|
21
|
-
/**
|
|
22
|
-
* Named schema+flow bundles `zitadel setup` can scaffold (#448: the prompt
|
|
23
|
-
* fires before any `.zitadel/` file is written; each preset maps to a
|
|
24
|
-
* pre-defined bundle the CLI copies on first setup). `password-first` is
|
|
25
|
-
* today's default; `passkey-first` puts a passkey ceremony on the login
|
|
26
|
-
* entry step with an email→password fallback path.
|
|
27
|
-
*/
|
|
28
|
-
declare const SETUP_PRESETS: readonly ["password-first", "passkey-first"];
|
|
29
|
-
type SetupPreset = (typeof SETUP_PRESETS)[number];
|
|
30
|
-
declare const DEFAULT_SETUP_PRESET: SetupPreset;
|
|
31
|
-
type DefaultConfigRenderOptions = {
|
|
32
|
-
builtinSchemaBase?: string;
|
|
33
|
-
userSchemaUrl?: string; /** Which bundle to render; defaults to {@link DEFAULT_SETUP_PRESET}. */
|
|
34
|
-
preset?: string;
|
|
35
|
-
};
|
|
36
|
-
declare function defaultHumanUserSchemaUrl(builtinSchemaBase?: string): string;
|
|
37
|
-
declare function getDefaultHumanUserSchema(options?: DefaultConfigRenderOptions): CreateSchemaBody;
|
|
38
|
-
declare function getDefaultLoginFlow(options?: DefaultConfigRenderOptions): CreateFlowDefinitionBodyFlowDefinition;
|
|
39
|
-
//#endregion
|
|
40
|
-
export { DEFAULT_SETUP_PRESET as a, SetupPreset as c, getDefaultLoginFlow as d, flowsReadmeContent as f, DEFAULT_SCHEMA_CONFIG_PATH as i, defaultHumanUserSchemaUrl as l, DEFAULT_FLOW_CONFIG_PATH as n, DefaultConfigRenderOptions as o, schemasReadmeContent as p, DEFAULT_FLOW_SCHEMA_URI as r, SETUP_PRESETS as s, DEFAULT_BUILTIN_SCHEMA_BASE as t, getDefaultHumanUserSchema as u };
|
|
41
|
-
//# sourceMappingURL=defaults-CSjBjBkQ.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"defaults-CSjBjBkQ.d.mts","names":[],"sources":["../src/readmes.ts","../src/defaults.ts"],"mappings":";;;;;;AAQA;;iBAAgB,oBAAA,CAAA;;;AAQhB;;iBAAgB,kBAAA,CAAA;;;cCIH,2BAAA;AAAA,cACA,uBAAA;AAAA,cACA,0BAAA;AAAA,cACA,wBAAA;;;ADPb;;;;;cCgBa,aAAA;AAAA,KAED,WAAA,WAAsB,aAAA;AAAA,cAErB,oBAAA,EAAsB,WAAA;AAAA,KAyBvB,0BAAA;EACV,iBAAA;EACA,aAAA,WA3CsC;EA6CtC,MAAA;AAAA;AAAA,iBAGc,yBAAA,CACd,iBAAA;AAAA,iBAKc,yBAAA,CACd,OAAA,GAAS,0BAAA,GACR,gBAAA;AAAA,iBAUa,mBAAA,CACd,OAAA,GAAS,0BAAA,GACR,sCAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"meta-schemas-8nlyVL7g.mjs","names":["flowDefinitionMetaSchema","userSchemaMetaSchema","userPropertyMetaSchema","authMethodsMetaSchema","authMethodMetaSchema"],"sources":["../meta-schemas/auth-method.json","../meta-schemas/auth-methods.json","../meta-schemas/flow-definition.json","../meta-schemas/user-property.json","../meta-schemas/user-schema.json","../src/meta-schemas.ts"],"sourcesContent":["","","","","","/**\n * The dialect meta-schemas `zitadel setup` copies into a project's\n * `.zitadel/meta/` so the flow/schema dialect is machine-readable offline:\n * editors validate flow files against `flow-definition.json` via a relative\n * `$schema` pointer, and agents read the dialect files before authoring\n * edits — no docs crawl, no hosted schema required.\n *\n * The files under `meta-schemas/` are committed copies of\n * `api/openapi/endpoints/schemas/*.json` (the source the server embeds);\n * a drift-audit test keeps them byte-identical in the monorepo.\n */\nimport authMethodMetaSchema from \"../meta-schemas/auth-method.json\" with {\n type: \"json\",\n};\nimport authMethodsMetaSchema from \"../meta-schemas/auth-methods.json\" with {\n type: \"json\",\n};\nimport flowDefinitionMetaSchema from \"../meta-schemas/flow-definition.json\" with {\n type: \"json\",\n};\nimport userPropertyMetaSchema from \"../meta-schemas/user-property.json\" with {\n type: \"json\",\n};\nimport userSchemaMetaSchema from \"../meta-schemas/user-schema.json\" with {\n type: \"json\",\n};\n\n/** Project-relative directory setup writes the meta-schemas to. */\nexport const META_SCHEMA_DIR = \".zitadel/meta\";\n\n/**\n * The `$schema` value scaffolded flow files carry, relative to\n * `.zitadel/flows/` — resolves to `{@link META_SCHEMA_DIR}/flow-definition.json`.\n */\nexport const FLOW_FILE_SCHEMA_REF = \"../meta/flow-definition.json\";\n\nexport type MetaSchemaFile = { name: string; body: object };\n\n/**\n * The dialect files to materialize, in write order. `auth-methods.json` and\n * `auth-method.json` are pulled in by `user-schema.json`'s relative `$ref`s —\n * without them the copied dialect cannot resolve offline.\n */\nexport function metaSchemaFiles(): ReadonlyArray<MetaSchemaFile> {\n return [\n { name: \"flow-definition.json\", body: flowDefinitionMetaSchema as object },\n { name: \"user-schema.json\", body: userSchemaMetaSchema as object },\n { name: \"user-property.json\", body: userPropertyMetaSchema as object },\n { name: \"auth-methods.json\", body: authMethodsMetaSchema as object },\n { name: \"auth-method.json\", body: authMethodMetaSchema as object },\n ];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AK4BA,MAAa,kBAAkB;;;;;AAM/B,MAAa,uBAAuB;;;;;;AASpC,SAAgB,kBAAiD;AAC/D,QAAO;EACL;GAAE,MAAM;GAAwB,MAAMA;GAAoC;EAC1E;GAAE,MAAM;GAAoB,MAAMC;GAAgC;EAClE;GAAE,MAAM;GAAsB,MAAMC;GAAkC;EACtE;GAAE,MAAM;GAAqB,MAAMC;GAAiC;EACpE;GAAE,MAAM;GAAoB,MAAMC;GAAgC;EACnE"}
|